Chapter 4 Choice set

library(reticulate)
use_condaenv("r-reticulate")

library(econR) # 經濟模型程式設計專題用

Decision Maker Problem

We need a storage object that can

  • represent the choice set X, with

  • its each element x a possible alternative for DM

In the following exercise, we want to construct object X with each element representing one x.

4.1 pRython major features

There are some major differences to notice for your first time amphibian experience:

  • Only = is used for object-value binding; Python doesn’t take <-.

  • Only [.] is used for element VALUE extraction and replacement in Python, which is equivalent to R’s [[.]].

  • Python element index starts from 0 instead of 1 in R

  • Object naming rule is the same as R, EXCEPT NO use of .

obj_R = c("Hello", "World.")
obj_R[1]

obj_R[1] = "Hi"
print(obj_R)
obj_py = ["Hello", "World."]
obj_py[0]

obj_py[0] = "Hi"
print(obj_py)
obj_R2 = list(
  first_word="Hello",
  second_word="World"
)
obj_R2[["first_word"]]
obj_py2 = {
  "first_word": "Hello",
  "second_word": "World"
}
  • R’s [[.]] extraction is equivalent to [.] in Python.

  • R’s [.] in list (which is the same as dictionary in Python) is equivalent to conduct (loop) comprehension (a mini-loop operation inside vector setup such as list/dictionary) operation in Python.


\[X=\{\mbox{1 apple}, \mbox{1 banana}\}\]

choiceSet_basic0 = 
  c(apple=1, banana=1)

# one alternative
choiceSet_basic0[1]

# the other alternative
choiceSet_basic0[2]
choiceSet_basic1 =
  list(apple=1, banana=1)
# one alternative
choiceSet_basic1[1]
# the other alternative
choiceSet_basic1[2]

4.2 Dictionary

dict({ })

  • The ONLY type of storage that allows users to give element name, called key.
choiceSet_basic0 = {"apple": 1, "banana": 1}

dictionary element CANNOT be checked out by its location:

  • elements are not indexed even we input them one-by-one.
# one alternative
choiceSet_basic0[0]

# the other alternative
choiceSet_basic0[1]
choiceSet_basic0["apple"]
choiceSet_basic0["banana"]
  • Python has only [.] extraction operator, but no [[.]] or $.

  • What . can be depends on the type of the object.

  • A Dictionary’s element value can be checked out only via its element key but not on its element position (you check the meaning of a word in a dictionary via the spelling of the word, not the sequence of the word entry.)

However, dictionary key can be numeric.

choiceSet_basic0_numKey = {
  1: {"apple": 1},
  2: {"banana": 1}
  }
  • Notice we also make index counting starting from 1 instead of 0 – another way to make it behave like R. (not useful though.)

  • dictionary can nest inside a dictionary

In fact, all vector types in Python:

  • allow element values of different types

  • allow nested structure,

which behaves like R: list.


4.3 List

list([ ])

list([])

list elements are indexed, but never named:

choiceSet_basic2_error = ["apple"=1, "banana"=1]
choiceSet_basic2_error = ["apple": 1, "banana": 1]
choiceSet_basic2 =
  [{"apple": 1}, {"banana": 1}]
  • list to give the ability of position indexation.

  • dictionary to give the ability of assigning element key.

4.4 Mutable v.s. Immutable

X = c(3, 53)
Y = X
pryr::address(X)
pryr::address(Y)
  • value c(3, 53) is stored at some memory address. Object X holds the memory address like holding the key to the value storage place. The relationship of an object holding the memory address to certain data storage place is called binding.

  • Both X and Y hold the same address reference.


If we change X[[1]] value, will Y[[1]] change?

X[[1]] = 44
pryr::address(X)
  • No. Y[[1]] will not be changed since X holds a different address reference now.

Almost all R objects are immutable; unable to change its element value in place.

  • In R, changing part of the object element value, in almost all cases, changes the address reference of the object – a whole new storage place is located and a new binding is formed. There is no change-in-place happening: the process is like copy old value c(3, 53) and change its first value 3 to 44, then find a new place to store c(44, 53), and give X the new place address reference.

  • An object stores its value under certain memory address. If that value can be changed without relocating to another memory address (i.e. value changed in place), then the object is mutable.

X = [3, 53]
Y = X
id(X)
id(Y)
X[0] = 44
id(X)

print(X)
print(Y) # Y因X改變也跟著改了。

To duplicate a mutable object, use .copy() method to obtain a shallow copy. This will break the link between the duplicate and the original objects.

X = [3, 53]
Y = X.copy()
id(X)
id(Y)
X[0] = 44
print(X)
print(Y)
X = {"banana": 1, "apple": 3}
Y = X.copy()

X["banana"]=0
print(X)
print(Y)

4.5 Define Function

\[u = \mbox{number_of_banana} * 5 + \mbox{number_of_apple}*3\]

X = list(
  list(
    banana=0, apple=1
  ),
  list(
    banana=1, apple=0
  )
)

Prototyping:

x = X[[1]]

u = {
  x$banana *5 + x$apple *3
}
U = function(x){
  assertthat::assert_that(
    x %in% X,
    msg="x should be an element in choice set X"
  )
  
  u = {
    x$banana *5 + x$apple *3
  }
  
  return(u)
}

However, %in% only works on atomic vector, not on R list. You can use:

library(magrittr)
X %>% purrr::has_element(x)
library(magrittr)
U = function(x){
  assertthat::assert_that(
    X %>% purrr::has_element(x),
    msg="x should be an element in choice set X"
  )
  
  u = {
    x$banana *5 + x$apple *3
  }
  
  return(u)
}

X = [
  {"banana": 1, "apple": 0},
  {"banana": 0, "apple": 1}
]

prototyping:

x = X[0]

u = x.get("banana") * 5 + x.get("apple") *3
def U(x):
    assert x in X, "x should be an element in choice set X"
    u = x.get("banana") * 5 + x.get("apple") *3
    return u

4.6 Indentation

A = 3

if(A < 5){
  print("I am small.")
} else {
  print("I am large.")
}
A = 3

if A < 5:
    print("I am small.")
else:
    print("I am large.")
  • python: indentation is equivalent to R: { } programming block.

  • All commands belong to the same level of programming block must start with the same indentation space.

  • Most Python programmer uses a multiple of FOUR spaces (i.e. two tabs) to define the coverage of programming block. (two spaces, or one tab, are saved to long commands that are better read when broken into separate lines.)

A = 300

if A < 5:
    print("I am small.") # Four spaces
else:
    print("I am large.") # Four spaces
    if A > 50:
        print( # Eight spaces
          "I am more than 50.") # TEN spaces (two for line breaking)

Exercise 4.1 Re-write the above codes in R to show your understanding of indentation.

In a language using indentation for block programming, it is a good practice:

  • ALWAYS start a NEWLINE with proper indentation at the ULTIMATE END of a programming block – this is especially important for the ending of a first level block which should end with a new line without identation.

BAD:

A = 300

if A < 5:
    print("I am small.") 
else:
    print("I am large.") 
    if A > 50:
        print( 
          "I am more than 50.") 

GOOD:

A = 300

if A < 5:
    print("I am small.") 
else:
    print("I am large.") 
    if A > 50:
        print( 
          "I am more than 50.") 
# start a newline to end the 1st level block

Exercise 4.2 Which one of the following will raise an error message? (In R, we need } else to let computer know the control flow has an else-block following. In Python, it’s proper indentation that informs computer such a thing.)

A = 3

if A < 5:
    print("I am small.") 
    
else:
    print("I am large.") 
    if A > 50:
        print( 
          "I am more than 50.") 
# start a newline to end the 1st level block
A = 3

if A < 5:
    print("I am small.") 

else:
    print("I am large.") 
    if A > 50:
        print( 
          "I am more than 50.") 
# start a newline to end the 1st level block

The visual structure of the code reflects its real structure. This makes it easy to grasp the skeleton of code just by looking at it.

Python coding styles are mostly uniform. In other words, you’re unlikely to go crazy from dealing with someone’s idea of aesthetically pleasing code. Their code will look pretty much like yours. — Quick Python Book, Mannings.

4.7 Comparison

Relational operator python: in is equivalent to R: %in% (for atomic vectors). As to negation,

x %in% X
!(x %in% X)
x in X
x not in X

Other relational operator that need more attention is:

A = c(9, 10)
B = c(9, 10)
pryr::address(A)
pryr::address(B) # has different memory address
A == B # elementwise comparison
A != B
identical(A, B) # object comparision
!identical(A,B)
  • R only compare its class and elements, not care about its memory reference
A = [9, 10]
B = A.copy()
id(A)
id(B)
A == B # object comparison in element values
A != B
A is B # object comparison in memory address (same address definitely same content)
A is not B

You can also use:

u = x["banana"] * 5 + x["apple"] *3

dict.get() not only gets value from x but also can set value when key is not available:

u = x.get("banana") *5 + x.get("apple") *3 + x.get("orange")
u = x.get("banana") *5 + x.get("apple") *3 + x.get("orange", 0)
print(u)
U(X[0])

Economists model choice set in a continuous space often such as:

\[P_x*x + P_y*y \leq I\], where, say, \(P_x=10, P_y=25, I=10000\) In this situation, DM (decision maker) chooses \((x,y)\) that fits the restriction; his choice set can be phrased as:

\[\textbf{C}=\{(x,y) : 10x+25y\leq 10000\}\] (Usually there are restrictions of \(x, y \geq 0\) as well. At the moment, for simplicity we ignore that.)


For computer to pick up the above choice set, we can design a validation function which:

  • check if \(c\) falls in \(\textbf{c}\); if yes, it returns \(c\). Otherwise, return missing value.
library(magrittr) # in order to use %>% operand

validate_c = function(c){
  assertthat::assert_that(
    is(c, "list") && 
      c %>% purrr::every(is.numeric) && 
      length(c)==2,
    msg = "Only numeric vector of length 2 is possible to sit inside the choice set."
  )
  
  if(
    10*c$x + 25*c$y <= 10000
  ){
    return(c)
  } else {
    return(NA)
  }
}
c1 <- list(x=10, y=20)
validate_c(c1)

c2 <- list(x=500, y=10000)
validate_c(c2)

c3 <- c(x=50, y=10)
try(validate_c(c3)) # 確保有error也不中斷script執行

For Python:

4.7.1 check type

If you aren’t familiar with Python types yet, initiate a type that you want to check:

dict0 = dict({})
dict0.__class__
  • It shows dict is the verbatim (語法) that Python symbols dictionary type.
c1 = {"x": 10, "y": 20}

type(c1) is dict
  • Use type(X) is type_verbatim to check if X is an object of type type_verbatim
dict0 = dict({})
dict0.__class__

type(dict0) is dict

Boolean operations:

TRUE & FALSE
TRUE && TRUE

TRUE | FALSE
TRUE || FALSE
True and True

True or False

Exercise:

dict1 = {"a": 5, "b": 0.7}

Check

  • if dict1["a"] and dict1["b"] are both numeric type.

  • dict1["a"] is not a string type.

When a condition is involved of multiple relational operations:

A = 15

# condition: is numeric
type(A) is int or type(A) is float

to form a flag with other conditions, each condition is better bounded by ( ) to avoid erroneous interpretation.

A = 15

# condition1: is numeric
type(A) is int or type(A) is float

# condition2: larger than 7
A > 7
A = 15
# flag True: A is a number larger than 7
(
  type(A) is int or type(A) is float
) and A > 7

If there is no ( ) to separate multiple conditional flags, Python deals with and first, then or:

False or True and False # return False, since it is equivalent to 
False or (True and False)

True or False and False # return True, since it is equivalent to 
True or (False and False)

# If you mean (A or B) and C
(False or True) and False

prototyping:

type(c1) is dict
is_numeric(c1["x"]) and is_numeric(c1["y"])
len(c1) == 2

In Python, there are only integer and float (non-integer) types. To check whether an object value is numeric, you need to either check both integer and float:

num0 = 5.5

type(num0) is int or type(num0) is float

or define an is_numeric function:

def is_numeric(x):
    return type(x) is int or type(x) is float

is_numeric(num0)
def validate_c(c):
    assert (
      type(c) is dict and 
      is_numeric(c["x"]) and is_numeric(c["y"]) and 
      len(c) == 2
      ), "Only dictionary of length 2 is possible to sit inside the choice set."
    
    if 10*c["x"] + 25*c["y"] <= 10000:
      return c
    else:
      return None

# EOF

When dealing with long operations, you can use ( ) to block the entire expression, inside ( ) you are allowed to break line before or after the operand.

Python does not give you as much coding style freedom as R:

c1 = {"x": 10, "y": 20}
c = validate_c(c1)
print(c)

c2 = {"x": 500, "y": 10000}
c = validate_c(c2)
print(c)
c is None # check if missing data

c3 = [50, 10]
try:
    c = validate_c(c3)
except:
    None # what to do when Error

# EOF

4.8 Loops

c = {"x": 10, "y": 20}

type(c["x"]) is int and type(c["y"]) is int
type(c) is int
  • Won’t work.
c = [10, 20]

type(c) is int
  • Won’t work.

  • Almost all basic operands in Python can not work on vectors (such as list, dictionary) directly, not like R:

c = c(10, 20)

is.numeric(c)

This is because in computer language basic operands are designed for atomic objects only.

Atom, defined as the smallest storage concept in a programming language, is vector in R, but is one single value in Python.

In R, the following are atoms:

"Hi"
c("Hi","How Are You")
c(187, 192)
TRUE
c(TRUE, FALSE)
  • Be careful, R: list is atom only if the element values are of the same type. If not, some coercion of type change will happen which can lead to errors or warnings.
numVec0 <- 15
numVec0 > 13

numVec1 <- c(10, 20, 0, 15)
numVec1 > 13
numList1 <- list(10, 20, 0, 15)
numList1 > 13

numList1 <- list(10, "a", FALSE, 15)
numList1 > 13

In Python, the following are atoms:

"Hi"
"How Are You"
187
192
True
False
numVec0 = 15
numVec0 > 13 # OK

numVec1 = [10, 20, 0, 15]
numVec1 > 13 # NOT OK

When working on non-atoms, loops are required.

Prototyping via R:

numVec1 = c(10, 20, 0, 15)
lgl1 = vector("logical", length(numVec1))
for(i in seq_along(numVec1)){
  lgl1[[i]] <- numVec1[[i]] > 13
}

lgl1
numVec1 = [10, 20, 0, 15]

lgl1 = [False] * len(numVec1) # create a list of 4 False
for i in range(len(numVec1)):
  lgl1[i] = numVec1[i] > 13

print(lgl1)

4.8.1 Comprehension: mini-loop

Since Python users must deal with loops over elements inside list and dictionary often, Python equips users with a mini-loop verbatim called list comprehension and dictionary comprehension:

  • When for loop result is to be saved as a list or dictionary, comprehension allows users to embed for loop inside list or dictionary to produce the result under the hood.

4.8.2 list comprehension

list comprehension:

lgl1 = ... # list type storage 
for i in iterate_generator:
  lgl1[i] = iteration command that generates i-th result
lgl1 = [ iteration command that generates i-th result 
    for i in iterate_generator]

numVec1 = [10, 20, 0, 15]

lgl1 = [False] * len(numVec1) # create a list of 4 False
for i in range(len(numVec1)):
  lgl1[i] = numVec1[i] > 13

print(lgl1)
  • No need to initiate a storage object. Just keep the iteration part:
for i in range(len(numVec1)):
  lgl1[i] = numVec1[i] > 13

Corresponding list comprehension:

lgl1 = [ numVec1[i] > 13 for i in range(len(numVec1))]

4.8.3 Loop over dictionary:

numDict1 = {"a": 15, "b": 20, "c": 0, "d": 15}

keys = list(numDict1.keys())
dict1 = dict({}) # initiate dict storage
for i in range(len(numDict1)):
    dict1.update(
      { keys[i]: numDict1[keys[i]] > 13}
      )

print(dict1)
  • dict.keys() generates keys of dict object.
numDict1 = {"a": 15, "b": 20, "c": 0, "d": 15}

dict1 = dict({}) # initiate dict storage
for (key_i, value_i) in numDict1.items():
    dict1.update({
      key_i : value_i > 13 })

print(dict1)
  • dict.items() generates (key, value) pair.

4.8.4 dictionary comprehension

Dictionary comprehension

dict1 = dict({}) # dict type storage 
for i in iterate_generator:
  key_i = iteration command that generates i-th key # key_i expression
  dict1.update({key_i: iteration command that generates i-th value})
dict1 = {
  key_i expression : iteration command that generates i-th value
    for i in iterate_generator }

numDict1 = {"a": 15, "b": 20, "c": 0, "d": 15}

keys = list(numDict1.keys())
dict1 = dict({}) # initiate dict storage
for i in range(len(numDict1)):
    dict1.update(
      { keys[i]: numDict1[keys[i]] > 13}
      )

print(dict1)
  • Focus only on iteration part:
for i in range(len(numDict1)):
    dict1.update(
      { keys[i]: numDict1[keys[i]] > 13}
      )
  • Dictionary comprehension:
numDict1 = {"a": 15, "b": 20, "c": 0, "d": 15}

keys = list(numDict1.keys())

dict1 = {
  keys[i]: numDict1[keys[i]] > 13 for i in range(len(numDict1))
}

print(dict1)

Dictionary is also equipped with another possible iteration:

dict1 = dict({})
for key_i, value_i in numDict1.items():
    dict1.update(
      {key_i: value_i > 13}
    )

print(dict1)
  • dict.items() generate (key_i, value_i) pairs

  • Focus on iteration part:

for key_i, value_i in numDict1.items():
    dict1.update(
      {key_i: value_i > 13}
    )
  • dictionary comprehension:
numDict1 = {"a": 15, "b": 20, "c": 0, "d": 15}

dict1 = {
  key_i: value_i > 13 for key_i, value_i in numDict1.items()
}

print(dict1)

4.8.5 Python loop efficiency

Python loop efficiency starts from its iterate generator, which acts like a function that produces iterate only when needed – no vector of iterates that occupied memory in advance.

# iterable
numDict1.keys() # iterate generator: any object that can make iteration happen -- iterable

# collection of iterates
list(numDict1.keys()) # show you what iterates can it generate as a list

# iterator
iter_numDict1Keys = iter(numDict1.keys()) # produce an iterator

next(iter_numDict1Keys)
next(iter_numDict1Keys)
next(iter_numDict1Keys)
next(iter_numDict1Keys)
  • In R, loops requires collection of iterates which take away memory space.

Several iterables:

  • range:
range(len(object))
for k in range(len(object)):
    ...

For dictionary object, the following are common iterables:

for k in numDict.keys():
    ...
for k, v in numDict.items():
    ...

4.9 Help

Python uses class.method, or namespace.function to check out the help menu.

help(objectType.method)
dict0 = {"apple":2, "banana":3}
dict0.__class__

help(dict.clear) # class.method
help(dict.fromkeys)
import math
help(math.acos) # namespace.function

Builtin functions can be checked directly:

help(range)

4.10 綜合練習

1. Foodpanda Sukiya

第4章綜合練習第一大題

1.1

# newPromotion1 
allLevels = 
  c(
    "新登場",
    "推薦套餐",
    "豪華雙饗丼",
    "牛丼類",
    "咖哩類",
    "豬肉丼類")

newPromotion1 <- list(
  "category"   = "新登場",
  "item"       = "番茄牛丼",
  "description"= "新鮮番茄的酸味,讓人食慾大開!",
  "price"      = 109
)
# newPromotion1 

1.2

recommend1 <- list(
  "category"   = "推薦套餐",
  "item"       = "蔥溫玉牛丼套餐",
  "description"= "蔥溫玉牛丼+自選套餐",
  "price"      = 149
)
# recommend1

1.3

sukiyaMenu <- list(
  newPromotion1,
  recommend1
)
# sukiyaMenu

1.4

sukiyaMenu2 <- sukiyaMenu
sukiyaMenu2[[1]]$category <-
  factor(c("牛丼類"), levels=allLevels)
sukiyaMenu2[[1]]$price <- 120

# sukiyaMenu2

1.5

sukiyaMenu3 <- purrr::transpose(sukiyaMenu)
sukiyaMenu3$category <- unlist(sukiyaMenu3$category)
sukiyaMenu3$item <- unlist(sukiyaMenu3$item)
sukiyaMenu3$description <- unlist(sukiyaMenu3$description)
sukiyaMenu3$price <- unlist(sukiyaMenu3$price)

sukiyaMenu3

1.6

sukiyaMenu4 <- sukiyaMenu

sukiyaMenu4[[1]][c("item","description","price")] -> options
sukiyaMenu4[[1]][c("item","description","price")] <- NULL
sukiyaMenu4[[1]]$options <- options

sukiyaMenu4[[2]][c("item","description","price")] -> options
sukiyaMenu4[[2]][c("item","description","price")] <- NULL
sukiyaMenu4[[2]]$options <- options

sukiyaMenu4
# sukiyaMenu4

2. Budget constraint

Consider the budget constraint:

\[\ P_{apple} * x_1 + P_{banana} * x_2 \leq I\]

Suppose \(I=1000, P_{apple}=5, P_{banana}=7\). Suppose \(x_1\) and \(x_2\) must be integer.

2.1

Suppose we decide to use [2,3] in Python to represent the alternative of 2 apples and 3 bananas. Given \(x1=3\), list all [x1, x2] alternative that satisfies the budget constraint in a list. That is your answer should look like [[3,0], [3,1], [3,2], ...] and each element is inside the choice set.

2.2

In Python, a list can append a new value or extend values such as:

list0 = [2, 3, 7]
list1 = list0.copy()

newList = [5, "b"]
list0.append(newList)
print(list0)

list1.extend(newList)
print(list1)

Construct a list that contains all the [x1, x2] pair inside the budget constraint. You may need to use append or extend methods above.

3. Atoms

par0 <- c(5)
par1 <- 3

2**par0 # **: 次方
2**par1

There is something wrong in the following. Fix it.

par0 = [5]
par1 = 3

2**par0
2**par1