
# In the book, we will represent matrices as lists of lists, 
# with each inner list having the same size and representing a row of the matrix. 
# If A is a matrix, then A[i][j] is the element in the ith row and the jth column. 

# we will typically use capital letters to represent matrices

A = [[1, 2, 3], 	# A has 2 rows and 3 columns
     [4, 5, 6]]

B = [[1, 2], 		# B has 3 rows and 2 columns
     [3, 4],
     [5, 6]]

# NOTE: we’ll call the first row of a matrix “row 0” and the first column “column 0.”

# ***********************************************************
# Shape of a matrix

def shape(A):
    num_rows = len(A)
    num_cols = len(A[0]) if A else 0 	# number of elements in first row
    return num_rows, num_cols

print(shape([[1,2,3], [4,5,6]]))
print(shape([[1,2,3]]) )
print(shape([]) )


# ***********************************************************
# Get a row or a column from a matrix

def get_row(A, i):
    return A[i] 		# A[i] is already the ith row

def get_column(A, j):
    return [A_i[j] 		# jth element of row A_i 
            for A_i in A] 	# for each row A_i

# *************************************************************
# Create a matrix with a given shape

def make_matrix(num_rows, num_cols, entry_fn):
    """returns a num_rows x num_cols matrix
       whose (i,j)th entry is entry_fn(i, j)"""

    return [[entry_fn(i, j) 		# given i, create a list [entry_fn(i, 0), ... 
             for j in range(num_cols)] 	#   ..., entry_fn(i, j), ... ]
             for i in range(num_rows)] 	# create one list for each i

# How to make an identity matrix:

# (1) define a function that return a value 1 if i==j and 0 if i!=j

def is_diagonal(i, j):
    """1's on the 'diagonal', 0's everywhere else"""
    return 1 if i == j else 0

# (2) Use this function to return the value in the elements in the matrix:

identity_matrix = make_matrix(4, 4, is_diagonal)
print(identity_matrix)


# How ML use matrices:
#
#    (1) we can use a matrix to represent a data set 
#
# Example, if you had the heights, weights and ages of 1,000 people;
# you could put them in a 1000x3 matrix:

data = [[70, 170, 40],
        [65, 120, 26],
        [77, 250, 19]
        # ....
       ]

# How ML use matrices:
#
#  (2) a matrix is a linear map
#
#      A nxk matrix maps a k-vector to an n-vector (space)
# 
#  (3) matrices can be used to represent binary relationships
#
#      Example: 
#
#          friendships = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3), (3, 4),
#                         (4, 5), (5, 6), (5, 7), (6, 8), (7, 8), (8, 9)]
#
#          can be represented in matrix form as:
#
#                     user 0  1  2  3  4  5  6  7  8  9
#
#          friendships = [[0, 1, 1, 0, 0, 0, 0, 0, 0, 0], # user 0
#                         [1, 0, 1, 1, 0, 0, 0, 0, 0, 0], # user 1
#                         [1, 1, 0, 1, 0, 0, 0, 0, 0, 0], # user 2
#                         [0, 1, 1, 0, 1, 0, 0, 0, 0, 0], # user 3
#                         [0, 0, 0, 1, 0, 1, 0, 0, 0, 0], # user 4
#                         [0, 0, 0, 0, 1, 0, 1, 1, 0, 0], # user 5
#                         [0, 0, 0, 0, 0, 1, 0, 0, 1, 0], # user 6
#                         [0, 0, 0, 0, 0, 1, 0, 0, 1, 0], # user 7
#                         [0, 0, 0, 0, 0, 0, 1, 1, 0, 1], # user 8
#                         [0, 0, 0, 0, 0, 0, 0, 0, 1, 0]] # user 9
#
#      Advantages with matrix representation:
#
#        (A)  with the matrix representation it is much quicker to check whether 
#             two nodes i and j are connected:
#
#            	   friendships[i][j] == 1
#
#        (B) to find the connections that a node has, you only need to inspect 
#            the column (or the row) corresponding to that node:
#
#              friends_of_five = [i                                         # only need
#                                 for i, is_fr in enumerate(friendships[5]) # to look at
#                                 if is_fr == 1]                            # one row



# NOTE:
#
#    All of the machinery we built here you get for free if you use NumPy. 
#    (You get a lot more too.)

