ts-python

Reproducibility II: Better notebooks and beyond

Discussion (Fri Jan 15, 2021; 3:30 PM EST)

Keywords: jupyter notebook; autoreload; heuristics for layout, display, splitting cells, kernel management, \%%magic; setup.py, libraries

Presenter James Powell james@dutc.io
Date Friday, January 15, 2021
Time 3:30 PM EST
print("Let's go!")

Game: Battlegame

The game “Battlegame” (patent-pending) is played as follows:

Task: model the game above, using what you have learnt.

Task: model same strategies, and build a framework for running these strategies against each other.

from itertools import product
from random import shuffle

def random_strategy(board_size=10):
    targets = [*product(range(board_size), range(board_size))]
    shuffle(targets)
    while True:
        yield targets.pop()

def linear_strategy(board_size=10):
    targets = [*product(range(board_size), range(board_size))]
    targets.reverse()
    while True:
        yield targets.pop()

Game: Rock, Paper, Scissors

Rules

The game “Rock, Paper, Scissors” is played as follows:

Task: write a function to evaluate the rules of the game.

# NOTE: for naming & design purposes, you may assume the players are directional
#       i.e., `a` is the Player
#             `b` is the Challenger
#       e.g., `rules` could return "player wins" or "player loses"
#              or it could "player wins" vs "challenger wins"
# QUESTION: how do you represent ties?
def rules(a, b):
    ''' return who wins, given shapes played by two players a and b '''
    pass

Task: write a framework that can evaluate a strategy and play the game for 10,000 rounds given a pairing of strategies.

from random import choice

def random_strategy():
    ''' randomly select a shape '''
    return choice(['rps'])

# other sample strategies…

# QUESTION: how do we track "history" here?
def beat_previous_play():
    ''' select the shape that would beat the opponent's previous play '''
    pass

def most_common_play(n=3):
    ''' select the most common shape from the opponent's previous N plays '''
    pass

games = [(random_strategy(), random_strategy()) for _ in range(10_000)]
results = [rules(a, b) for a, b in games]

Game: Poker

from enum import Enum, auto
from functools import total_ordering
from collections import namedtuple, Counter
from itertools import product, islice, tee, combinations
from random import shuffle

@total_ordering
class Suits(Enum):
    Clubs    = 0
    Diamonds = auto()
    Hearts   = auto()
    Spades   = auto()
    def __lt__(self, other):
        return self.value < other.value
    def __eq__(self, other):
        return self.value == other.value
    def __hash__(self):
        return hash(self.value)

    def __str__(self):
        return ['\N{black club suit}',
                '\N{black diamond suit}',
                '\N{black spade suit}',
                '\N{black heart suit}',][self.value]

@total_ordering
class Faces(Enum):
    Two   = 0
    Three = auto()
    Four  = auto()
    Five  = auto()
    Six   = auto()
    Seven = auto()
    Eight = auto()
    Nine  = auto()
    Ten   = auto()
    Jack  = auto()
    Queen = auto()
    King  = auto()
    Ace   = auto()
    def __lt__(self, other):
        return self.value < other.value
    def __eq__(self, other):
        return self.value == other.value
    def __hash__(self):
        return hash(self.value)
    def __str__(self):
        return '2 3 4 5 6 7 8 9 10 J Q K A'.split()[self.value]

nwise = lambda g, n=2: zip(*(islice(g, i, None) for i, g in enumerate(tee(g, n))))

class HandType(namedtuple('HandType', 'predicate')):
    __call__ = lambda s, *a, **kw: s.predicate(*a, **kw)
@total_ordering
class Hands(Enum):
    RoyalFlush = HandType(
        lambda cs: Hands.StraightFlush(cs) and max(cs, key=lambda c: c.face).face == Faces.Ace
    )
    StraightFlush = HandType(
        lambda cs: Hands.Straight(cs) and Hands.Flush(cs)
    )
    Flush = HandType(
        lambda cs: Counter(c.suit for c in cs).most_common(1)[0][-1] == 5
    )
    FourOfAKind = HandType(
        lambda cs: Counter(c.face for c in cs).most_common(1)[0][-1] == 4
    )
    Straight = HandType(
        lambda cs: all(y.face.value - x.face.value == 1
                       for x, y in nwise(sorted(cs, key=lambda c: c.face)))
    )
    FullHouse = HandType(
        lambda cs: [c for _, c in Counter(c.face for c in cs).most_common(2)] == [3, 2]
    )
    TwoPair = HandType(
        lambda cs: [c for _, c in Counter(c.face for c in cs).most_common(2)] == [2, 2]
    )
    ThreeOfAKind = HandType(
        lambda cs: Counter(c.face for c in cs).most_common(1)[0][-1] == 3
    )
    TwoOfAKind = HandType(
        lambda cs: Counter(c.face for c in cs).most_common(1)[0][-1] == 2
    )
    HighCard = HandType(
        lambda cs: True
    )

    def __call__(self, *args, **kwargs):
        return self.value(*args, **kwargs)
    def __lt__(self, other):
        return self.value < other.value
    def __eq__(self, other):
        return self.value == other.value
    def __hash__(self):
        return hash(self.value)

class Card(namedtuple('Card', 'face suit')):
    def __str__(self):
        return f'{self.face!s}{self.suit!s}'

class Hand(namedtuple('HandBase', 'cards best_hand')):
    @classmethod
    def from_cards(cls, *cards):
        best_hand = [hand_type for hand_type in Hands if hand_type(cards)]
        return cls(cards, best_hand)

deck = [Card(f, s) for f, s in product(Faces, Suits)]
shuffle(deck)

flop  = [deck.pop(), deck.pop(), deck.pop()]
hand  = [deck.pop(), deck.pop()]
turn  = [deck.pop()]
river = [deck.pop()]

h = [*flop, *hand, *turn, *river]
print(f'{Hand.from_cards(*flop, *hand, *turn, *river).best_hand = }')

#  from contextlib import contextmanager
#  from time import perf_counter
#  @contextmanager
#  def timed():
#      try:
#          start = perf_counter()
#          yield
#      finally:
#          stop = perf_counter()
#          print(f'Elapsed (\N{greek capital letter delta}t): {stop - start:.2f}s')

#  with timed():
#      all_possible_hands = Counter(Hand.from_cards(*cs).best_hand
#                                   for cs in islice(combinations(deck, 5), 10_000_000))
#  all_possible_hands = Counter()
#  for idx, cs in islice(combinations(deck, 5)):
#      all_possible_hands[Hand.from_cards(*cs).best_hand] += 1
#  print(f'{all_possible_hands = }')

mutating tuple in Python

class Loan:
    pass

class Bond(Loan):
    pass
from tracemalloc import start, take_snapshot

def poke(x):
    pass

class Dataset:
    def __init__(self, data):
        self.data = data
    def poke(self):
        for x in self.data:
            poke(x)
    def __getitem__(self):
        pass

class Element:
    __slots__ = *'xy',
    def __init__(self, x, y):
        self.x, self.y = x, y

# restricted computation domain
# - numpy.ndarray
# - pandas.DataFrame
start()
before = take_snapshot()
xs     = Dataset([Element(0, 0) for _ in range(2_000)])
after  = take_snapshot()

xs.poke()

for line in after.compare_to(before, 'lineno')[:1]:
    print(line)

#  from sys import getsizeof
#  print(f'{getsizeof(Element(10, 20)) = }')
#  print(f'{Element(10, 20).__dict__   = }')
#  print(f'{(10 ** 2000).bit_length()  = :,}')
from numpy import array
xs = array([1, 2, 3, 2**65])
print(f'{xs = }')
from contextlib import contextmanager
from time import perf_counter

@contextmanager
def timed(msg):
    try:
        start = perf_counter()
        yield
    finally:
        end = perf_counter()
        print(f'{msg:<20} \N{greek capital letter delta}t: {end-start:.4f}s')

size = 5_000_000

from random import randint
xs = [randint(-1_000, 1_000) for _ in range(size)]

from numpy.random import randint
ys = randint(-1_000, 1_000, size=size)

with timed('list: `sum`'):
    sum(xs)
with timed('list: dumb loop'):
    total = 0
    for x in xs:
        total += x
with timed('numpy'):
    ys.sum()
with timed('numpy/unboxing'):
    total = 0
    for x in ys:
        total += x
from numpy import array
xs = array([1, 2, 3])
print(f'{xs.__array_interface__["data"][0] = :#_x}')
from numpy import array
from numpy.lib.stride_tricks import as_strided

def setitem(t, i, v):
    xs = array([], dtype='uint64')
    loc = xs.__array_interface__['data'][0]
    idx = id(t) - loc
    xs = as_strided(xs, strides=(1,), shape=(idx + 1,))
    ys = as_strided(xs[idx:], strides=(8,), shape=(4,))
    zs = as_strided(ys[3:], strides=(8,), shape=(i + 1,))
    ys[2] += max(0, i - (end := len(t)) + 1)
    zs[min(i, end):] = id(v)

t = 0, 1, 2, None, 4, 5
print(f'{t = }')
setitem(t, 3, 3)
print(f'{t = }')
from contextlib import contextmanager
from time import perf_counter

@contextmanager
def timed(msg):
    try:
        start = perf_counter()
        yield
    finally:
        end = perf_counter()
        print(f'{msg:<20} \N{greek capital letter delta}t: {end-start:.4f}s')

from numpy.random import normal

xs = normal(size=(1_000_000, 50))
with timed('.sum(axis=0)'):
    xs.sum(axis=0)

xs = normal(size=(50, 1_000_000))
with timed('.sum(axis=1)'):
    xs.sum(axis=1)
#  with timed('.T.sum(axis=0)'):
#      with timed('  .T.copy()'):
#          ys = xs.T.copy()
#      with timed('  ys.sum(axis=0)'):
#          ys.sum(axis=0)
#  print(f'{xs.strides = }')
#  print(f'{ys.strides = }')
from pandas import Interval, array
xs = array([
    Interval(0, 4),
    Interval(4, 8),
])

print(f'{dir(xs) = }')

@classmethod & @property

collections.namedtuple & dataclasses.dataclass

metaprogramming

from collections import namedtuple
from dataclasses import dataclass
from enum import Enum, auto

# eval/exec
class T(namedtuple('T', 'x y')):
    pass
print(f'{T(10, 20)   = }')
print(f'{T(10, 20).x = }')

# class decorator
class Base:
    pass

@dataclass
class T(Base):
    x : int
    y : int
    z : int = 100
print(f'{T(10, 20) = }')
print(f'{help(T)   = }')

# metaclass/__init_subclass__
class T(Enum):
    X = auto()
    Y = auto()

class Base:
    def __init_subclass__(cls):
        print(cls)

class Derived(Base):
    pass

print(f'{T.X           = }')
print(f'{T.Y           = }')
print(f'{T["X"] is T.X = }')
print(f'{[*T]          = }')
def __build_class__(*args, bc=__build_class__, **kwargs):
    print(f'__build_class__(*{args}, **{kwargs})')
    return bc(*args, **kwargs)
import builtins
builtins.__build_class__ = __build_class__

class T:
    pass

class X:
    pass

import json
import pandas
from collections import Counter

c1 = Counter('aabccccddddd')
c2 = Counter('aaaaabbcccdd')
print(f'{c1      = }')
print(f'{c2      = }')
print(f'{c1 + c2 = }')
print(f'{c1["a"] = }')