# NumPy Notes
><i class="fa fa-file-text"></i> [NumPy Tutorial](https://www.w3schools.com/python/numpy/default.asp)
:::success
:bulb: **Axis demonstration**
axis = 0 | Vertical effect | Joins rows (stack one array on top of the other)
axis = 1 | Horizontal effect | Joins columns (place arrays side-by-side)
:::
```python!
import numpy as np
arr1 = np.array([[[1,2,3],[4,5,6]],[[1,2,3],[4,5,6]]]) #initializing 3d array
arr2 = np.array([1,2,3,4,5,6,7,8])
print('3d array:\n', arr1) #print array
print('dimensions:', arr1.ndim) #print dimensions
print('@ index 0:\n', arr1[0]) #printing 2d array at index 0
print('@ index [0,1]:', arr1[0,1]) #printing array at [0,1]
print('@ index [0,1,2]:', arr1[0,1,2]) # printing value at [0,1,2]
print('Last element from 2nd dim: ', arr1[0,0,-1]) # negatives to access last value
print('Slicing last array 1 to 2: ', arr1[1,1,1:3]) #slicing is exclusive
print('Slicing first array 0 to 1: ', arr1[0,0,:2]) #slicing with end bounds vice versa
print('Slicing index 3 from the end to index 1 from the end: ', arr2[-3:-1]) #slicing with negatives
print('Slicing and stepping by 2: ', arr2[1:5:2]) #stepping
arr3 = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]])
print('Slicing index 1 to index 4', arr3[0:2, 1:4]) #for rows 0 to 2, cols 1 to 4
print('Every other value', arr2[::2]) #every other value with stepping
print('Data type: ', arr1.dtype) # print the arr content type
arr4 = np.array(['dog', 'pig', 'cat'], dtype ='U') #initializing arrays with specific type
arr5 = arr2.astype('f') #copying another array and changing data type
arr6 = arr4.copy() #copies another array; copies own data
arr7 = arr4.view() #views another array; views don't own data and is tied to the OG
print('OG:', arr4)
arr4[0] = 'FAT'
print('Changed OG:', arr4)
print('copy:' , arr6)
print('view:', arr7) #change view also changes the OG
print(arr6.base) # checks for base array
print(arr1.shape) #print shape
arr8 = np.array([1,2,3,4], ndmin = 5) #makes an array with specific dimension
print('5d array:', arr8)
arr9 = arr2.reshape(2,4) #reshaping an array; reshapes are tied to the OG
arr10 = arr2.reshape(2,2,-1) #use -1 for np to calculate reshaping for you
arr11 = arr10.reshape(-1) #-1 flattens an array to 1D
print('Printing each value in arr1: ')
for x in arr1: #iterate through arrays with for loops
for y in x:
for z in y:
print(z)
print('Printing with nditer: ')
for x in np.nditer(arr1): # alt iteration shortcut
print(x)
print('Printing changed data type: ')
for x in np.nditer(arr1, flags = ['buffered'], op_dtypes=['S']): #change each element data type with nditer
print (x)
print('Printing index values: ')
for idx, x in np.ndenumerate(arr1): #ndenumerate to get index values
print (idx, x)
arr12 = np.concatenate((arr4,arr6)) #concatenate arrays
arr13 = np.stack((arr4,arr6), axis = 1) #stack is the same as concat but adds a new dimension
arr14 = np.vstack((arr4,arr6)) #another way for axis=0
arr15 = np.hstack((arr4,arr6)) #another way for axis=1
arr16 = np.dstack((arr4,arr6)) #stacking for 3d
arr17 = np.array_split(arr2,3) #splitting an array --- times
print("Split array 3 sections:", arr17) #print split array
print ('The first split array:', arr17[0]) #access split arrays
arr18 = np.vsplit(arr14,2) #opposite for vstack; vice versa for hsplit and hstack
a = np.where(arr2 == 2) #where() tells you where element matches
b = np.searchsorted(arr2, 2, side = "left") #returns index value where the element should be inserted to maintain sort
#use side = to specify which side to start search
c = np.searchsorted(arr2, [2, 4, 6]) #you can also searchsort a set of numbers
arr19 = np.array([3,2,1,6,8,88])
print("Sorted array:", np.sort(arr19)) #sorts and prints array; works with strings, booleans, 2d arrays...
d = [True, True, False, True, False, False] #created a list of booleans
print('Boolean driven array:', arr19[d]) #prints array according to the boolean list
#this is called filter array
```
# NumPy Random
### NORMAL DATA DISTRIBUTION


### BINOMIAL DISTRIBUTION


>[!Tip]
>Normal distribution is continuous while binomial is discrete
### POISSON DISTRIBUTION


>[!Tip]
> - Poisson is discrete and normal is continuous
> - Poisson has infinite amount of outcomes while binomial only has 2
### UNIFORM DISTRIBUTION


### LOGISTIC DISTRIBUTION


>[!Tip]
>Compared to normal: logistic distribution has more area under the tails, meaning it represents more possibility of occurrence of an event further away from mean
### MULTINOMIAL DISTRIBUTION

### EXPONENTIAL DISTRIBUTION


>[!Tip]
>Poisson deals w/ num of occurrences of an event in a time period; exponential deals w/ the time btw events
### CHI SQUARE DISTRIBUTION


### RAYLEIGH DISTRIBUTION


### PARETO DISTRIBUTION


### ZIPF DISTRIBUTION


```python!
from numpy import random
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
arr1 = np.array([1,2,3,4,5])
a = random.randint(100) #generates random int from 0 to 100
b = random.rand() #generates random float from 0 to 1
c = random.randint(100, size = (5)) #generates a 1D array with 5 random int
d = random.randint(100,size = (5,2)) #random 2D array!
e = random.choice([1,5,99,0]) #randomly picks an element from the array
f = random.choice([0,99,84,3], size = (3,5)) #randomly generates 2D array using listed elements and specified size
g = random.choice([3, 5, 7, 9], p=[0.1, 0.3, 0.6, 0.0], size=(100)) #you can control probabilities by assigning a num for each element 0-1
#0 means never occur; 1 means allways occur
random.shuffle(arr1) #randomly shuffles array; changes the OG
h = random.permutation(arr1) #shuffles array; does NOT change the OG
sns.displot([0,1,0,1,2,3,4,5], kind = 'kde') #create displot
plt.show() #shows displot; kde makes the graph a line, otherwise its a histogram
i = random.normal(loc =1, scale =2, size =(2,3)) #RANDOM NORMAL DISTRIBUTION size 2x3 with mean at 1 and standard deviation of 2
j = random.binomial(n=10, p=0.5,size= 10) #RANDOM BINOMIAL DISTRIBUTION 10 trials for coin toss generate 10 data points
k = random.poisson(lam=2, size=10)#RANDOM POISSON DISTRIBUTION random 1x10 distribution for occurrence 2
l = random.uniform(size=(2,3)) #RANDOM UNIFORM DISTRIBUTION 2x3 uniform distribution sample
m = random.logistic(loc=1, scale=1, size=(2,3)) #RANDOM LOGISTIC DISTRIBUTION 2x3 samples from a logistic distribution with mean at 1 and stddev 2.0
n = random.multinomial(n=6, pvals=[1/6, 1/6, 1/6, 1/6, 1/6, 1/6]) #RANDOM MULTINOMIAL DISTRIBUTION dice roll exampple
o =random.exponential(scale=2, size=(2, 3)) #RANDOM EXPONENTIAL DISTRIBUTION 2.0 scale with 2x3 size
p = random.chisquare(df=2, size=(2, 3)) #RANDOM CHI SQUARE DISTRIBUTION degree of freedom 2 with size 2x3
q = random.rayleigh(scale=2, size=(2, 3)) #RANDOM RAYLEIGH DISTRIBUTION scale of 2 with size 2x3
r= random.pareto(a=2, size=(2, 3)) #RANDOM PARETO DISTRIBUTION shape of 2 with size 2x3
s = random.zipf(a=2, size=(2, 3)) #RANDOM ZIPF DISTRIBUTIONparameter 2 with size 2x3
```
# NumPy Ufunc
:::success
:bulb: **Creating a ufunc**
To create your own ufunc, you have to define a function, like you do with normal functions in Python, then you add it to your NumPy ufunc library with the frompyfunc() method.
The frompyfunc() method takes the following arguments:
1. function - the name of the function.
2. inputs - the number of input arguments (arrays).
3. outputs - the number of output arrays.
:::
:::success
:bulb: **Rounding Decimals**
There are primarily five ways of rounding off decimals in NumPy
1.Truncation
2.Fix
3.Rounding
4.Floor
5.Ceil
:::
```python!
import numpy as np
from numpy import random as rand
x = [1,2,3]
y = [4,5,6]
z = []
a = [-1,0,-3,4]
g = [0,0,0,1,1,0]
b = rand.rand(5) *10 -5
for i, j in zip(x,y): #without ufunc, you can use zip() to add
z.append(i + j)
z = np.add(x,y) #array addition with ufunc!
def myadd(x,y): #define ufunc
return x+y
myadd = np.frompyfunc(myadd,2,1) #creating your own addition ufunc
print(type(np.add)) #validates that add is a ufunc
if type(np.add) == np.ufunc: #checks if add is a ufunc
print('add is ufunc')
else:
print('add is not ufunc')
np.subtract(x,y) #array subtraction
np.multiply(x,y) #array multiplication
np.divide(x,y) #array division
np.power(x,y) #array power of values
np.remainder(x,y) #array remainder
np.mod(x,y) #array modulus; same as remainder except for when there are negatives
np.divmod(x,y) #returns both quotient(first array) and mod(second array) answers
np.absolute(a) #absolute values the array
print('array: ', b)
print('truncate: ', np.trunc(b)) #chops off the decimal
print('fix: ', np.fix(b)) #basically the same as truncating
print('around: ', np.around(b)) #checks if decimal >=5
print('floor: ', np.floor(b)) #rounds down
print('ceil: ', np.ceil(b)) #rounds up
np.log2(x) #finds log at base 2 for each element; there's also log10 and log
np.sum([x,y]) #sums everything into one value; also works with axis
np.cumsum(x) #partial adding the elements in the array
np.prod([x,y]) #multiplies everything into one value; works with axis; cumprod()
np.diff(a, n=2)#subtracts two successive elements n num of times
np.lcm(5,3) #finds the lowest common multiple btw two nums
np.lcm.reduce(y) #finding the lcm of an array
#same stuff for gcd
print(np.pi) #look its pi
pies = np.array([np.pi/2, np.pi/3, np.pi/4, np.pi/5])
print(np.sin(pies)) #sin, cos, tan, arcsin, arccos, arctan trig np functions!
print(np.rad2deg(pies)) #convert rad to deg; np.deg2rad works too
base = 3
perp = 4
p = np.hypot(base, perp) #Find the hypotenues for 4 base and 3 perpendicular
np.sinh(np.pi/2) #hyperbolic trig sinh,cosh, tanh, arcsinh, arccosh, arctanh
np.unique(g) #creates unique array (one digit only present once); only works with 1d
one = np.array([1,1,1,2])
two = np.array([2,2,26])
np.union1d(one,two) #unique array with 2 arrays (only 2)
np.intersect1d(one,two, assume_unique=True) #find values only present in both arrays
np.setdiff1d(one,two, assume_unique=True) #present in the first array but not the second
np.setxor1d(set1, set2, assume_unique=True) #not present in both arrays
```
# Brute Force with NumPy
```Python!
import hashlib
import numpy as np
import string
import time
# Function to hash a password
def hash_password(password):
return hashlib.sha256(password.encode()).hexdigest()
# Dictionary attack (optional)
wordlist = np.array(["12345", "password", "admin", "qwerty"])
hashed_list = np.vectorize(hash_password)(wordlist)
# User input
password = input("Enter a password to simulate cracking: ").strip()
target_hash = hash_password(password)
# Brute-force function
def brute_force(target_hash, max_len=4):
charset = list(string.ascii_lowercase + string.digits)
attempts = 0
start_time = time.time()
while True:
# Random length between 1 and max_len
length = np.random.randint(1, max_len + 1)
# Generate a random guess
guess = ''.join(np.random.choice(charset, size=length))
guess_hash = hash_password(guess)
attempts += 1
if guess_hash == target_hash:
end_time = time.time()
return attempts, guess, end_time - start_time
# Run the brute-force attack
attempts, cracked_password, duration = brute_force(target_hash, max_len=4)
# Output
print(f"\n✅ Password '{cracked_password}' cracked after {attempts} attempts in {duration:.2f} seconds.")
```
>[time=Mon, Jun 9, 2025]
# Tensors Notes
:::info
:information_source: **Info**
- Data Structures similar to arrays and matrices
- Used for encoding inputs and outputs of a model
- Similar to NumPy ndarrays, but tensors run on GPU and other hardware accelerators
:::
```python=
import torch
import numpy as np
#CREATING TENSORS
data = [[1,2],[3,4]] #directly from data
x_data = torch.tensor(data)
np_array = np.array(data) #from numpy arrays
x_np = torch.from_numpy(np_array)
#from other tensors
x_ones = torch.ones_like(x_data) #retains properties of x_data; tensor filled with ones with x_data dimin
x_rand = torch.rand_like(x_data, dtype = torch.float) #overrides datatype of x_data; random float tensor with x_daya dimin
#with random or constant values
shape = (2,3,) #this is a tuple in tensor dimensions
rand_tensor = torch.rand(shape) #random tensor with shape dimin
ones_tensor = torch.ones(shape) #tensor filled with ones in shape dimin
zeros_tensor = torch.zeros(shape) #tensor filled with zeros in shape dimin
#ATTRIBUTES OF A TENSOR
tensor = torch.rand(3,4)
tensor.shape #shape of the tensor
tensor.dtype #data type of tensor
tensor.device #device tensor stored on
#OPERATIONS ON TENSORS
if torch.cuda.is_available(): #check if cuda(gpu) is available
device = torch.device("cuda")
#indexing and slicing
tensor = torch.randint(0, 10, (4,4), dtype=torch.int32) #random int tensor
tensor = torch.ones(4,4)
print(f"First row: {tensor[0]}")
print(f"Last row: {tensor[-1,...]}")
print(f"First column: {tensor[:,0]}")
print(f"Last column: {tensor[...,-1]}")
tensor[:,1] = 0
#joining tensors
t1 = torch.cat([tensor,tensor,tensor], dim = 1) #dim is like axis
#arithmetic operations
#below is matrix multiplication, all y vars output the same
#tensor.T returns the transpose of the tensor, swaps the two dimin or the last two dimin
y1 = tensor @ tensor.T
y2 = tensor.matmul(tensor.T)
y3 = torch.rand_like(y1)
torch.matmul(tensor, tensor.T, out=y3)
#below is element-wise product, all z vars output the same
z1 = tensor * tensor
z2 = tensor.mul(tensor)
z3 = torch.rand_like(z1)
torch.mul(tensor, tensor, out=z3)
#single-element tensors
agg = torch.sum(tensor) #turns tensor into a single value
agg_item = agg.item() #turns agg into python numerical value
#in-place operations
#operations that store the result into the operand are called in-place
#denoted by _ suffix
tensor.add_(5)
#BRIDGE WITH NUMPY
#tensors on the CPU and NumPy arrays can share their underlying memory locations, and changing one will change the other.
#tensor to numpy array
t = torch.ones(5)
n = t.numpy()
print(f"tensor: {t} ")
print(f"numpy: {n}")
t.add_(1)
print(f"tensor after: {t} ")
print(f"numpy after: {n}")
#numpy array to tensor
n = np.ones(5)
t = torch.from_numpy(n)
print(f"tensor: {t} ")
print(f"numpy: {n}")
np.add(n,1,out=n)
print(f"tensor after: {t} ")
print(f"numpy after: {n}")
```
>[time=Wed, Jun 11, 2025]