<jupyter_start><jupyter_text>Fundamentals of coding in PythonThis material is mostly adapted from the [official python tutorial](https://docs.python.org/3/tutorial/), Copyright 2001-2019, Python Software Foundation. It is used here under the terms of the [Python License](https://docs.python.org/3/license.html). Also, part of this introduction if based on the Python introduction from last year of <NAME>, created under the BSD 3-Clause License (https://github.com/AJueling/python_climate_physics). Exercises can be found at the end of the Notebook Variables and printingYou can use Python as a calculator, try a simple calculation such as `3*4+6/8`. If there are multiple calculations in one cell and you run the cell (`Shift+Enter`), by default only the last result is shown. Therefore, if you want to make the output of multiple lines visible, you need the command `print(variable)`. There are certain words that you can not use as variable names. These identifiers are used as reserved words, or keywords of the language. They must be spelled exactly as written here:    False      class      finally    is         return    None       continue   for        lambda     try    True       def        from       nonlocal   while    and        del        global     not        with    as         elif       if         or         yield    assert     else       import     pass    break      except     in         raise<jupyter_code># comments are anything that comes after the "#" symbol
a = 2       # assign 2 to variable a
b = "hello" # assign "hello" to variable b

# how to we see our variables?
print(a)
print(b)
print(a,b)<jupyter_output><empty_output><jupyter_text>In this example you see that we have different types of variables. All variables are objects. Every object has a type, which we call a __class__. Examples are text (__strings__) and numbers (__integers__ or __floats__). In your computations, variables often need to be of a certain class. To find out what type your variables are, use the command `type`<jupyter_code>print(type(a))
print(type(b))

# You can also check the type with the keyword 'is'
print(type(a) is int)
print(type(a) is str)<jupyter_output><empty_output><jupyter_text>You can print integers/floats and strings together by concatenating them with a comma `,` , plus sign `+` or  `%` sign. You have to be careful when printing different variables together that are of a different class (try for example `print(a+b)` to see what error message you get).<jupyter_code>print(b,"! This is the number", a)
print(b + " ! This is the number " + str(a))
print("%s ! This is the number %i " %(b, a)) # define the class of the variables after the %-sign, 
                                            # s=string, i=integer, f=float<jupyter_output><empty_output><jupyter_text>If a variable is a float or an integer, you can execute computations within the `print` command. The printed output is the result of the computation. However, multiplying a string by a number repeats the string this number of times.<jupyter_code>print(a+2)
print(a*b)
print((a+1)*b)<jupyter_output><empty_output><jupyter_text>There are several built-in functions you can use in Python. Some are for integers and some are for floats. You can access them trough the command ``variable.method``.  It you type the variable name, followed by `tab`, IPython gives you all the methods that are available for this variable.<jupyter_code># this returns the method itself, so what the method does 
b.capitalize
# this calls the method
b.capitalize()
# there are lots of other methods<jupyter_output><empty_output><jupyter_text>The following a built-in functions which are always available in your namespace once you open a Python interpreter    abs() dict() help() min() setattr() all() dir() hex() next() slice() any()    divmod() id() object() sorted() ascii() enumerate() input() oct() staticmethod()    bin() eval() int() open() str() bool() exec() isinstance() ord() sum() bytearray()    filter() issubclass() pow() super() bytes() float() iter() print() tuple()    callable() format() len() property() type() chr() frozenset() list() range()    vars() classmethod() getattr() locals() repr() zip() compile() globals() map()    reversed() import() complex() hasattr() max() round() delattr() hash()    memoryview() set()     MathsBasic arithmetic and Boolean logic is part of the core Python library. Here, we will give all the important commands.<jupyter_code># addition / subtraction
1+1-5
# multiplication
5 * 10
# division
1/2
# that was automatically converted to a float
type(1/2)
# exponentiation
2**4
# modulo operator
3%2
# rounding
round(9/10)
# built in complex number support
(1+2j) / (3-4j)
# logic --> these will come in handy when we look at conditionals and loops later in this Notebook
True and True
True and False
True or True
True or False
(not True) or (not False)<jupyter_output><empty_output><jupyter_text>Lists and indexingLists are one of the core Python data structures. You can fill them with strings or with numbers. Lists come in handy when we have larger datasets or when we are performing multiple calculations in loops (we will practice this later). Lists have their own methods/built-in functions that are very useful. Looking at lists gives us the opportunity to practice with Python indexing. This may be a bit counterintuitive at first, but practicing helps. This indexing is often different in other coding languages. The first element of a list has the index `0` and the last element of a list has index `len(list)-1` (so the length of the list minus 1). There are also several ways to select only certain elements of a list.<jupyter_code>l = ['dog', 'cat', 'mouse','fish']
print("The class of l =", type(l))
print(l)
print("The length of list l = ",len(l))
print("The first element of l =",l[0])
print("The last element of l =",l[len(l)-1])
print(l[len(l)]) # now you will get an error due to the indexing<jupyter_output><empty_output><jupyter_text>To create a list of numbers, a convenient command is `range()`. In between the brackets you put the beginning and end of the range, and a stepsize.1. `range(stop)` = range from 0 to (stop-1) with stepsize = 12. `range(start,stop)` = range from start to (stop-1) with stepsize = 13. `range(start,stop,step)` = range from start to (stop-1) with stepsize = stepStart, stop, step must be all integers. You can convert a `range()` to a list with the command `list()`.<jupyter_code>print(range(10)) # range creates a range of numbers from 0 to the 4 (always the last number-1)
r = list(range(10)) # convert this range to a list the command 
type(r)
print(r)
print(list(range(2,10,2)))<jupyter_output><empty_output><jupyter_text>There are many different ways to interact with lists. Exploring them is part of the fun of Python.__list.append(x)__ Add an item to the end of the list. Equivalent to a[len(a):] = [x].__list.extend(L)__ Extend the list by appending all the items in the given list. Equivalent to a[len(a):] = L.__list.insert(i, x)__ Insert an item at a given position. The first argument is the index of the element before which to insert, so a.insert(0, x) inserts at the front of the list, and a.insert(len(a), x) is equivalent to a.append(x).__list.remove(x)__ Remove the first item from the list whose value is x. It is an error if there is no such item.__list.pop([i])__ Remove the item at the given position in the list, and return it. If no index is specified, a.pop() removes and returns the last item in the list. (The square brackets around the i in the method signature denote that the parameter is optional, not that you should type square brackets at that position. You will see this notation frequently in the Python Library Reference.)__list.clear()__ Remove all items from the list. Equivalent to del a[:].__list.index(x)__ Return the index in the list of the first item whose value is x. It is an error if there is no such item.__list.count(x)__ Return the number of times x appears in the list.__list.sort()__ Sort the items of the list in place.__list.reverse()__ Reverse the elements of the list in place.__list.copy()__ Return a shallow copy of the list. Equivalent to a[:].Don't assume you know how list operations work!<jupyter_code># Here is an example of pop, we will come to while-loops in a minute
while r:
    p = r.pop()
    print('p:', p)
    print('r:', r)
# "add" two lists
x = list(range(5))
y = list(range(10,15))
# range syntax: either range(stop), range(start,stop), or range(start, stop, step)
# start, stop, step must be integers
z = x + y
z
# access certain items from a list
print('The first element of z = ', z[0])
print('The last element of z = ', z[-1])
print('The first 3 elements of z are ', z[:3])
print('The last 3 elements of z are', z[-3:])
print('Element 2 until the third last are', z[1:-2])  
print('Every second element of z (starting from the first)', z[0::2])
print('The middle is, skipping every other item', z[5:10:2])
# this index notation also applies to strings
name = '<NAME>'
print(name[:4])
# you can also test for the presence of items in a list using 'in'
5 in z<jupyter_output><empty_output><jupyter_text>Note that, lists are not meant for math! They don't have a datatype.<jupyter_code>z[4] = 'fish'
z<jupyter_output><empty_output><jupyter_text>__MEMORIZE THIS SYNTAX!__ It is central to so much of Python and often proves confusing for users coming from other languages.In terms of set notation, Python indexing is _left inclusive, right exclusive_. If you remember this, you will never go wrong. Other datatypesApart from lists, there are also __tuples__ and __dictionaries__ each have their own advantages and disadvantages.1. __Tuples__ = similar to lists, but they are *immutable*, which means that they can't be extended or modified. What is the point of this? Generally speaking: to pack together inhomogeneous data. Tuples can then be unpacked and distributed by other parts of your code. Tuples may seem confusing at first, but with time you will come to appreciate them.1. __Dictionaries__ = an extremely useful data structure. It maps __keys__ to __values__. Dictionaries are unordered!Here are some examples of tuples and dictionaries!<jupyter_code># tuples are created with parentheses, or just commas
a = ('Ryan', 33, True)
b = 'Takaya', 25, False
type(b)
# tuples can be indexed like arrays
print(a[1]) # not the first element!
# and tuples can be unpacked
name, age, status = a
print(f'name: {name}, age: {age}, status: {status}')  # this an f-string, which allows you to enter 
                                                      # variables in strings
# DICTIONARIES
# different ways to create dictionaries
d = {'name': 'Ryan', 'age': 33}
e = dict(name='Takaya', age=25)
e
# access a value
d['name']<jupyter_output><empty_output><jupyter_text>Square brackets ``[...]`` are Python for "get item" in many different contexts.<jupyter_code># test for the presence of a key
print('age' in d)
print('height' in e)
# try to access a non-existant key
d['height']
# add a new key
d['height'] = (5,11) # a tuple
d
# keys don't have to be strings
d[99] = 'ninety nine'
d
# iterate over keys
for k in d:
    print(k, d[k])
# better way
### python 2
### for key, val in d.iteritems()
for key, val in d.items():
    print(key, val)<jupyter_output><empty_output><jupyter_text>For-, while- and if-loopsYou have already seen a few examples here and there, but there are several ways to do multiple computations consequetively. We call these loops. Loops use indentation (a `tab` or 4 spaces) to define the beginning and end of the loops. There are three different types of loops:1. `if` = do some operations, only when a certain condition is met. This is often also called a conditional.2. `for` =  repeat a chunk of code a predetermined number of times.3. `while` = repeat a chunk of code  as long as a certain condition is satisfied.Here we will show some examples:<jupyter_code># example of if-statement 
x = 100
if x > 0:    # set a condition
    print('Positive Number')
elif x < 0:  # give an alternative
    print('Negative Number')
else:    # the rest will go into this part 
    print ('Zero!')

# another example of an if-statement
# indentation is MANDATORY
# blocks are closed by indentation level
if x > 0:
    print('Positive Number')
    if x >= 100:     # from the indentation you can see that this if-statement is part of the first
        print('Huge number!')
# Example for-loop
# in for-loops, the amount of computations is predetermined
# to determine the amount of computations, we often use 'range'
for i in range(5): 
    print(i)
     
# make a while-loop 
count = 0
while count < 10:
    # bad way
    # count = count + 1
    # better way
    count += 1 # this means the same as count = count + 1
print(count)
# You can use loops to iterate over lists
for pet in ['dog', 'cat', 'fish']:
    print(pet, len(pet))
# with the enumerate method we can get the position at the same time
for i, pet in enumerate(['dog', 'cat', 'fish']):
    print(i, pet, len(pet))
x = list(range(5))
y = list(range(10,15))
# iterate over two lists together uzing zip
for item1, item2 in zip(x,y):
    print('first:', item1, 'second:', item2)<jupyter_output><empty_output><jupyter_text>Loops can be very computationally expensive, i.e. the demand a lot of memory and the Python code can become very slow. You can use lists to do the same thing as in a loop, but this is a lot faster and demands less memory. We will practice with this later, but just keep it in mind!<jupyter_code># a cool python trick: list comprehension
squares = [n**2 for n in range(5)]
squares<jupyter_output><empty_output><jupyter_text>FunctionsIf you have long and complex codes, it can be very useful to organize your code into reusable elements. For example, if you find yourself cutting and pasting the same or similar lines of code over and over,you probably need to define a _function_ to encapsulate that code and make it reusable. This is very useful if you want to perform the same computation at different stages in the code.An important principle in programming in **DRY**: "don't repeat yourself".Repetition is tedious and opens you up to errors. Strive for elegance and simplicity in your programs.You can define a new function with the command `def`. Just as in loops, we use indentation to tell Python when a function starts and ends. Functions take some inputs ("arguments" which are one or multiple variables) and do something in response. Usually functions return something, but not always.When you define a new function with `def`, Python only saves this function. The computations will only happen when the function is called.Within the function, you can provide a description of what the function does. This must be put between `"""...."""`.  You can later call on this description by typing `help(function_name)`.<jupyter_code># define a function
def say_hello():
    """Return the word hello."""  # this is a documentation string describing what the function does
                                  # can be called with the function help()
    return 'Hello'
# Now Python has only 
# functions are also objects
type(say_hello)
help(say_hello) # call the description of the function
# You call the function by putting brackets at the end
say_hello()
# assign the result to something
res = say_hello()
res
# Now we define a funcation that takes some arguments
def say_hello_to(name):
    """Return a greeting to `name`"""
    return 'Hello ' + name
# You have to provide inputs for these arguments when you call them
say_hello_to('World')<jupyter_output><empty_output><jupyter_text>When giving the input for the function, pay attention to the class of the variables. Otherwise you will get an error message.<jupyter_code>say_hello_to(10)
# redefine the function
def say_hello_to(name):
    """Return a greeting to `name`"""
    return 'Hello ' + str(name)
say_hello_to(10)
# take an optional keyword argument
def say_hello_or_hola(name, spanish=False):
    """Say hello in multiple languages."""
    if spanish:
        greeting = 'Hola '
    else:
        greeting = 'Hello '
    return greeting + name
print(say_hello_or_hola('Ryan'))
print(say_hello_or_hola('Juan', spanish=True))
# flexible number of arguments
def say_hello_to_everyone(*args):
    return ['hello ' + str(a) for a in args]
say_hello_to_everyone('Ryan', 'Juan', 'Xiaomeng')<jupyter_output><empty_output><jupyter_text>Pure vs. Impure FunctionsFunctions that don't modify their arguments or produce any other side-effects are called [_pure_](https://en.wikipedia.org/wiki/Pure_function). Functions that modify their arguments or cause other actions to occur are called _impure_.Below is an impure function.<jupyter_code>def remove_last_from_list(input_list):
    input_list.pop()
names = ['Ryan', 'Juan', 'Xiaomeng']
print(names)
remove_last_from_list(names)
print(names)
remove_last_from_list(names)
print(names)<jupyter_output><empty_output><jupyter_text>We can do something similar with a pure function.In general, pure functions are safer and more reliable.<jupyter_code>def remove_last_from_list_pure(input_list):
    new_list = input_list.copy()
    new_list.pop()
    return new_list
names = ['Ryan', 'Juan', 'Xiaomeng']
new_names = remove_last_from_list_pure(names)
print(names)
print(new_names)<jupyter_output><empty_output><jupyter_text>NamespacesIn python, a [namespace](https://docs.python.org/3/tutorial/classes.htmlpython-scopes-and-namespaces) is a mapping between variable names and Python objects. You can think of it like a dictionary.The namespace can change depending on where you are in your program. Functions can "see" the variables in the parent namespace, but they can also redefine them in a private scope.<jupyter_code>name = 'Ryan'

def print_name():
    print(name)

def print_name_v2():
    name = 'Kerry'
    print(name)
    
print_name()
print_name_v2()
print(name)<jupyter_output><empty_output><jupyter_text>A more complex function: Fibonacci SequenceThe Fibonacci sequence is the 1,1,2,3,5,8..., the sum of each number with the preceding one. Write a function to compute the Fibonacci sequence of length n. (Hint, use some list methods.)<jupyter_code>def fib(n):
    l = [1,1]
    for i in range(n-2):
        l.append(l[-1] + l[-2])
    return l
fib(10)<jupyter_output><empty_output><jupyter_text>ClassesWe have worked with many different types of Python objects so far: strings, lists, dictionaries, etc. These objects have different attributes and respond in different ways to the built-in functions (`len`, etc.)_How can we make our own, custom objects?_ Answer: by defining classes. A class to represent a hurricane<jupyter_code>class Hurricane:
    
    def __init__(self, name):
        self.name = name
h = Hurricane('florence')
h<jupyter_output><empty_output><jupyter_text>Our class only has a single attribute so far:<jupyter_code>h.name<jupyter_output><empty_output><jupyter_text>Let's add more, along with some input validation:<jupyter_code>class Hurricane:
    
    def __init__(self, name, category, lon):
        self.name = name.upper()
        self.category = int(category)
        
        if lon > 180 or lon < -180:
            raise ValueError(f'Invalid lon {lon}')
        self.lon = lon
        
h = Hurricane('florence', 4, -46)
h
h.name
h = Hurricane('ryan', 5, 300)<jupyter_output><empty_output><jupyter_text>Now let's add a custom method:<jupyter_code>class Hurricane:
    
    def __init__(self, name, category, lon):
        self.name = name.upper()
        self.category = int(category)
        
        if lon > 180 or lon < -180:
            raise ValueError(f'Invalid lon {lon}')
        self.lon = lon
    
    def is_dangerous(self):
        return self.category > 1
f = Hurricane('florence', 4, -46)
f.is_dangerous()<jupyter_output><empty_output><jupyter_text>Magic / dunder methodsWe can implement special methods that begin with double-underscores (i.e. "dunder" methods), which allow us to customize the behavior of our classes. ([Read more here](https://www.python-course.eu/python3_magic_methods.php)). We have already learned one: `__init__`. Let's implement the `__repr__` method to make our class display something pretty.<jupyter_code>class Hurricane:
    
    def __init__(self, name, category, lon):
        self.name = name.upper()
        self.category = int(category)
        
        if lon > 180 or lon < -180:
            raise ValueError(f'Invalid lon {lon}')
        self.lon = lon
        
    def __repr__(self):
        return f"<Hurricane {self.name} (cat {self.category})>"
    
    def is_dangerous(self):
        return self.category > 1
f = Hurricane('florence', 4, -46)
f<jupyter_output><empty_output><jupyter_text>Exercises1. Calculate the fourth power of all integers to 15 using either a `for` loop or a `while` loop. 2. Create a list of the third power of all integers to 10 using a list comprehension. 3. Remove the second and fifth item from the list you just created. 4. Loop over all squares until 15 and only print those that are divisible (without remainder) by 3. Print the following strings `(number)^2=(number squared) is divisible by 3.` replacing the paranthesized parts. 5. Print the sum of all integers up to `n` for all `n<10` and print "Done!" below when done. 6. Invert the list `a = list(range(10,51,10))` using a loop. 7. Given string of undefined length >5, return the middle three letters. 8. Given a string, exchange the first and last characters. 9. Given a string with mixed capital and lower case letters and numbers, order them with lowercase letters first, numbers second, uppercase letters last. *(Hint: google built-in commands for lists that can distuingish between numbers, uppercase and lowercase letters).* 10. Given the list `list1 = [5, 20, 15, 20, 25, 50, 20]`, find value 20 in the list, and if it is present, replace it with 200. Only update the first occurrence of a value. 11. Create a function that can accept two arguments (name and age) and print its values. 12. Write a function `func1()` such that it can accept a variable length of  an argument and print all values of the arguments on a seperate line. 13. Write a function that generates a Python list of all the even numbers between a lower bound `a` and an upper bound `b`. In Python, we can create a nested function inside a function. We can use the nested function to perform complex tasks multiple times within another function or avoid loop and code duplication.14. Create an inner function to calculate the addition in the following way    1. Create an outer function that will accept two parameters `a` and `b`    2. Create an inner function inside an outer function that will calculate the addition of `a` and `b`    3. At last, the outer function will add 5 into addition and return it 15. Write a function that returns the largest item from a given list of numbers. Use a built-in function of Python.  SolutionsThese are possible solutions, there are certainly other valid ways to achieve the desired outcomes.<jupyter_code># 1. Calculate the fourth power of all integers to 15 using either a `for` loop or a `while` loop.
for i in range(15):
    print(f'{i:2d}^4 = {i**4}')  # the ':2d' specifies the output format to integer using 5 spaces
# 2. Create a list of the third power of all integers to 10 using a list comprehension.
[n**3 for n in range(10)]
# 3. Remove the second and fifth item from the list you just created.
A = [n**3 for n in range(10)]
A.pop(5)  # the order matter here
A.pop(2)
A
# 4. Loop over all squares until 15 and only print those that are divisible (without remainder) by 3. 
# Print the following strings `The square of (number)=(number squared) and is divisible by 3.`
# replacing the paranthesized parts.
for i in range(15):
    if i**2%3==0:
        print(f'{i:2d}^2={i**2:3d} is divisible by 3.')
        # the ':2d' specifies the output format to integer using 5 spaces
# 5. Print the sum of all integers up to `n` for all `n<10` and print "Done!" below when done.
n = 0
for i in range(10):
    n += i
    print(i, n)
print('Done!')
# 6. Invert the list `a = list(range(10,51,10))` using a loop.
a = list(range(10,51,10))
print(a)
b = []
for i in range(len(a)):
    b.append(a[-i-1])
print(b)
# without a loop you can use the reversed method:
c = list(reversed(a))
print(c)
# 7. Given string of undefined length >5, return the middle three letters.
# two strings with odd and even numbers of characters
string1 = 'abcdefg'
string2 = 'helloWorld'

n1, n2 = len(string1), len(string2)
print(n1, n2)
print(string1[int(n1/2)-1:int(n1/2)+2])
print(string2[int(n2/2)-1:int(n2/2)+2])
# 8. Given a string, exchange the first and last characters.
string = string2[-1] + string2[1:-1] + string2[0]
print(string)
# 9. Given a string with mixed capital and lower case letters and numbers, 
# order them with lowercase letters first, numbers second, uppercase letters last.
string = '>>>HelloWorld123 !?!'
lower, numbers, upper, symbols = '', '', '', ''
for l in string:
    if l.islower():
        lower +=l
    elif l.isnumeric():
        numbers +=l
    elif l.isupper():
        upper +=l
    else:
        symbols += l
print(lower+numbers+upper)
print('note how we did not use the symbols:', symbols)
# 10. Given the list `list1 = [5, 20, 15, 20, 25, 50, 20]`, find value 20 in the list, and if it is present,
# replace it with 200. Only update the first occurrence of a value.
list1 = [5, 20, 15, 20, 25, 50, 20]
print(list1)
index = list1.index(20)
list1[index] = 200
# 11. Create a function that can accept two arguments name and age and print its value
def first_function(name, age):
    """ simply print name and age"""
    print(f'name: {name}, age = {age} years')
    
# you can call this function in three different ways
first_function('Gandalf', 2019)
first_function('Gandalf', age=2019)
first_function(name='Gandalf', age=2019)
# first_function(name='Gandalf', 2019)  # this does not work
# 12. Write a function func1() such that it can accept a variable length of 
# argument and print all arguments value
def func1(*arguments):
    """ prints all arguments passed on a separate line """
    for a in arguments:
        print(a)
func1('Hello', 3)
func1(3.1415, 1.618, 2.718)
# 13. Write a function that generates a Python list of all the even numbers between a lower bound `a` 
# and an upper bound `b`.
def even_numbers(a,b):
    """ generates list of even numbers between a and b """
    even_list = []
    for i in range(a,b):
        if i%2==0:  even_list.append(i)
    return even_list

print(even_numbers(3,16))

# a list comprehension is more pythonic:
a, b = 3, 16
print([i for i in range(a,b) if i%2==0])
# In Python, we can create a nested function inside a function. 
# We can use the nested function to perform complex tasks multiple times 
# within another function or avoid loop and code duplication.
# 14. Create an inner function to calculate the addition in the following way
#     A. Create an outer function that will accept two parameters a and b
#     B. Create an inner function inside an outer function that will calculate the addition of a and b
#     C. At last, the  outer function will add 5 into addition and return it
def nested_function(a, b):
    """ adds 5 to the sum of a and b """
    def add(c,d):
        """ add c and d """
        return c+d
    return add(a,b)+5
nested_function(3,4)
# 15. Write a funcion that returns the largest item from a given list of numbers. 
# Use a built-in function of Python.
def find_max(some_list):
    """ finds the maximum of the list some_list """
    return max(some_list)
find_max([1,6,12,75,20,60])<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09348800231280717, 'fraction_numerical': 0.016659439144261348, 'mean_word_length': 4.0813441057657, 'pattern_counts': {'":': 0, '<': 115, '<?xml version=': 0, '>': 122, 'https://': 6, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '26', 'dir': 'jupyter-structured-clean-dedup', 'id': '483844', 'license': 'MIT', 'n_tokens_mistral': 8420, 'n_tokens_neox': 7951, 'n_words': 3752, 'path': 'Workshop 1b.ipynb', 'repo_name': 'JannekeKrabbendam93/Python-Climate-Physics-2021', 'size': '27687'}