---
# System prepended metadata

title: 'Day 1 week 2:'

---

# Day 1 week 2:
## We learn about intermediate Python:
(https://colab.research.google.com/drive/1VzVTPTkfRzoxcOouZDkpqbeu7eqUvLXQ)
 Those stuffs:
 - List coprehensive
     - a = [a for a in range(10) if a % 2 == 0]
     - a = [a x a for a in range(10)]
 - Iterable and Generator # use to save memory, a generator is like a prepared factory but haven't produce anything
     - def generate_range(n):
         i = 0
         while i < n:
             yield i
             i += 1
     - automated testing via assert
     - randomness # if you all ready set up a random seed the result will be the same but still in randomness
         import random
         random seed(10) (set up a random seed)
         four_uniform_randoms = [random.random() for _ in range(4)]
         random.randrange(a) # choose randomly in range (0 -> a - 1)
         random.randrange(a, b) # choose randomly in range (a -> b-1)
         random.shuffle
         random.choice
        lottery_numbers = range(49)
        winning_numbers = random.sample(lottery_numbers, 6)  # [16, 36, 10, 6, 25, 9]
 - zip and unpacking
 - pointer in Python
 - Python decorators
    Some use cases:
        Time functions
        Debugging
        Is the user logged in?
 - Recursive
     Greatest common division
      def GCD(a, b):
         if b == 0:
             return a
         else:
             return GCD(b%a, a)
     Fibo numbers
      def  Fibo(n):
         if n == 1 or n ==2:
             return 1
         else:
             return Fibo(n-1, n-2)
     Factorial n!
      def fac(n):
          if n == 1:
              return 1
          else:
              return fac(n-1)
     Tower of Hanoi
      def Tower(n, a, b, c):
           if n == 1:
               return "a to c"
            else:
               return "n-1 to b, n to c, n -1 to c"
                       
               
             
 - Regular expression ( to find parttern in text)
    import re
    
    Character classes		
    .                                                        any character except newline	
    \w \d \s	                                             word, digit, whitespace	
    \W \D \S	                                             not word, digit, whitespace	
    [abc]	                                                 any of a, b, or c	
    [^abc]	                                                 not a, b, or c	
    [a-g]                                                    character between a & g		
    
    
    Anchors			
    ^abc$	                                                 start / end of the string	
    \b	                                                     word boundary	
    
    Character	Description	Example
    ?	Match zero or one repetitions of preceding	"ab?" matches "a" or "ab"
    *	Match zero or more repetitions of preceding	"ab*" matches "a", "ab", "abb", "abbb"...
    +	Match one or more repetitions of preceding	"ab+" matches "ab", "abb", "abbb"... but not "a"
    {n}	Match n repetitions of preceding	"ab{2}" matches "abb"
    {m,n}	Match between m and n repetitions of preceding	"ab{2,3}" matches "abb" or "abbb"
    
    Quantifiers & Alternation	
    a* a+ a?	                                             0 or more, 1 or more, 0 or 1
    a{5} a{2,}	                                             exactly five, two or more    
    a{1,3}	                                                 between one & three
    a+? a{2,}?	                                             match as few as possible
    ab	cd
            
    Escaped characters        
    . * \	\                                                is used to escape special chars. * matches *
    \t \n \r	                                             tab, linefeed, carriage return
    


# Day 2 week 2
## Challenging day
    We've been introduced many important hard concepts to swallow
    Big O notation (use to calculate time a function run wen n go to infinity)
        0(1)
        0(n)
        0(logn)
        0(nlogn)
        0(n**c)
        0(c**n)
    Recursion ( use to give elegant solutions )
        Factorial
            def fac(n):
                while n>0:
                if n = 1:
                    return 1
                elif n = 2:
                    return a to b
                            a to c
                            b to c
                else:
                    return fac(n-1)*n
        
        Tower of Hanoi
            def tower(n, a, b, c):
                while n>0:
                if n == 1:
                    return a to c
                elif n = 2:
                    return a to b, a to c, b to c
                else:
                    return tower(n-1)
        Greatest common division
        Fibonacci number
    Sorting algorithm (reduce big 0 when use sort method) 
        Quick sort
        Merge sort
    Object oriented programming
        instance attribute
        instance method
        class notebook:
            def __init__(self, cover, pages):
                self.cover = cover
                self.pages = pages
                
            def write(self, color): (instance method)
                return white
            
            def look(beautiful):
                return "this is a wonderful notebook"

# Day 3 week 2:
    (another interesting day)

## NUMPY(alternative Tenserflow) (want some speed: try np.blablabla)
    Python built in top of C (C access directly to memory)
    Numpy c extension

Create just one data type in a list in NUMPY
Same size memory for faster searching (numpy)
Different size memory for flexibility (search Python without numpy)

code in numpy
x = np.array([[[]]]) (x is an array)
an array which is a list and that list is numbers aranged in  dimesions represent a scalar , vector, matrix, 3D tensor (like a picture)

## Basic
x.ndim (n dimention of x)
x.shape 1,1,1 is dimesion 1 have 1 element, demension 2 have 1 element, demension 3 have 1 element
x.size is all element the array have
x.dtype int64, float64, bool

# Indexing
    x = np.array([1, 2, 3]) # ndim 2, shape 1x3, size 3, dtype int64
    print ("x: ", x) 
    print ("x[0]: ", x[0]) # print out: x[0]: 1
    x[0] = 0
    print ("x: ", x) # print out: [0  2  3]

# Slicing
    x = np.array([[1,2,3,4], [5,6,7,8], [9,10,11,12]])
    print (x)
    [[1 2 3 4]
     [5 6 7 8]
     [9 10 11 12]]
    print ("x column 1: ", x[:, 1]) # "x column 1": 2 6 10
    print ("x row 0: ", x[0, :]) # x row 0: 1 2 3 4
    print ("x rows 0,1 & cols 1,2: \n", x[0:2, 1:3]) # [[2 3]
                                                        [5 7]]

# Integer array indexing
    print (x)
    rows_to_get = np.array([0, 1, 2]) # take rows 0 1 2 == first second third
    print ("rows_to_get: ", rows_to_get)
    cols_to_get = np.array([0, 2, 1]) # take columns 0 2 1 == first third second
    print ("cols_to_get: ", cols_to_get)

# Combine sequences above to get values to get
    print ("indexed values: ", x[rows_to_get, cols_to_get]) # combine row and column produce six pairs which is the indexs of x we need to print out which is (0, 0), (1, 2), (2, 1)
    it should print:
    indexed values: [1 7 10]

# Boolean array indexing
    x = np.array([[1, 2], [3, 4], [5, 6]])
    print ("x:\n", x) # should print out: [[1 2]
                                            [3 4]
                                            [5 6]]
    print ("x > 2:\n", x > 2) # should print out: [[False False]
                                                    [True True]
                                                    [True True]]
    print ("x[x > 2]:\n", x[x > 2]) [3 4]
                                    [5 6]
    print(sum(x > 2)) [2 2]
    np.sum(x > 2) 4

##Arithmetic
# Basic math
    x = np.array([[1,2], [3,4]], dtype=np.float64)
    y = np.array([[1,2], [3,4]], dtype=np.float64)
    print ("x + y:\n", np.add(x, y)) # or x + y [[2 4]
                                                [6 8]]
    print ("x - y:\n", np.subtract(x, y)) # or x - y [0 0]
                                                    [0 0]
    print ("x * y:\n", np.multiply(x, y)) # or x * y [[1 4]
                                                        [9 16]
                                                    

# Dot product (USE in matrixs multiplication: reproduce every dots and built a new matrix: m*n * n*p = m * p) 
    a = np.array([[1,2,3], [4,5,6]], dtype=np.float64) # we can specify dtype
    b = np.array([[7,8], [9,10], [11, 12]], dtype=np.float64)
    c = a.dot(b)
    
# c = np.dot(a, b)
    print (f"{a.shape} · {b.shape} = {c.shape}") [2 2]
        print (c))

# Axis operations
# Sum across  a dimension
    x = np.array([[1,2],[3,4]])
    print (x)
    print ("sum all: ", np.sum(x)) # adds all elements
    print ("sum axis=0: ", np.sum(x, axis=0)) # sum column + column (across rows)
    print ("sum axis=1: ", np.sum(x, axis=1)) # sum row + row (across columns)

# Min/max
    x = np.array([[1,2,3], [4,5,6]])
    print ("min: ", x.min()) # 1
    print ("max: ", x.max()) # 6
    print ("min axis=0: ", x.min(axis=0)) 1 2 3
    print ("min axis=1: ", x.min(axis=1)) 1 4

    Broadcasting
    Broadcasting in NumPy follows a strict set of rules to determine the interaction between the two arrays:

    Rule 1: If the two arrays differ in their number of dimensions, the shape of the one with fewer dimensions is padded with ones on its leading (left) side.
    Rule 2: If the shape of the two arrays does not match in any dimension, the array with shape equal to 1 in that dimension is stretched to match the other shape.
    Rule 3: If in any dimension the sizes disagree and neither is equal to 1, an error is raised.

# Broadcasting
    x = np.array([1,2]) # vector
    y = np.array(3) # scalar
    z = x + y
    print ("z:\n", z)
    
    
    
## Advance Matrix
# Transposing
    x = np.array([[1,2,3], [4,5,6]])
    print ("x:\n", x)
    print ("x.shape: ", x.shape)
    y = np.transpose(x, (1,0)) # flip dimensions at index 0 and 1
    print ("y:\n", y)
    print ("y.shape: ", y.shape)

# Reshaping
    x = np.array([[1,2,3,4,5,6]])
    print (x)
    print ("x.shape: ", x.shape)
    y = np.reshape(x, (2, 3))
    print ("y: \n", y)
    print ("y.shape: ", y.shape)
    z = np.reshape(x, (2, -1))
    print ("z: \n", z)
    print ("z.shape: ", z.shape)

# Adding dimensions
    x = np.array([[1,2,3],[4,5,6]])
    print ("x:\n", x)
    print ("x.shape: ", x.shape)
    y = np.expand_dims(x, 1) # expand dim 1
    print ("y: \n", y)
    print ("y.shape: ", y.shape)   # notice extra set of brackets are added

# Removing dimensions
    x = np.array([[[1,2,3]],[[4,5,6]]])
    print ("x:\n", x)
    print ("x.shape: ", x.shape)
    y = np.squeeze(x, 1) # squeeze dim 1
    print ("y: \n", y)
    print ("y.shape: ", y.shape)  # notice extra set of brackets are gone
    
    
# Day 4 week 2:
    Data base story
## Data base
    - Table
    - Column
    - Row
    - Schema
    - Primary key, foreign key
    The relationship between two tables should be one to many or many to one
    AVOID many to many
## SQL syntax
    SELECT DISTINCT column, AGG_FUNC(column)...
    FROM mytable
    INNER/LEFT/RIGHT/FULL JOIN another_table
    ON mytable.column = another_table.column
    GROUP BY column
    HAVING constraint_expression
    ORDER BY column ASC / DESC
    LIMIT count OFFSET count
    
    
# Day 5 week 2:
    Tough day
    Today teacher fixed some problems in 116 Python problems https://repl.it/~
    And we spend more than three ours to do a test
    Which helped me understand decoration in Python better:
        def deco(function):
            def wrapper(*args, **kargs):
                print("something, before")
                print("something, after before but still before function execute")
                function(*args, **kargs)
                print("after function executed")
            return wrapper
    
    we use:
    @deco
    function(*args, **kargs)
    
    When we call 
    function()
    
    And the result should be:
    something, before
    something, after before but still before function execute
    (function do sth)
    after function executed
    
    However, after being decorated, function() has gotten very confused about its identity. 
    It now reports being the wrapper() inner function
    We use this: @functools.wraps(func)
    we put it here:
    def deco(function):
    ->>>> @functools.wraps(func)
          def wrapper(*args, **kargs):
                print("something, before")
                print("something, after before but still before function execute")
                function(*args, **kargs)
                print("after function executed")
            return wrapper
            
            