We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
cost estimator fails to run
To reproduce:

- Launch the cost estimator on binder https://mybinder.org/v2/gh/jupyterhub/zero-to-jupyterhub-k8s/master?filepath=doc/ntbk/draw_function.ipynb
- Run the first cell
- Observe a traceback similar to the following:

```
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-1-de8d1e48d121> in <module>
----> 1 from z2jh import cost_display, disk, machines
      2 machines

~/doc/ntbk/z2jh/__init__.py in <module>
      1 """Helper functions to calculate cost."""
      2 
----> 3 from .cost import cost_display, disk
      4 from .cost import machines_list as machines

~/doc/ntbk/z2jh/cost.py in <module>
     39                 thisrow.append(jj.text.strip())
     40         rows.append(thisrow)
---> 41     df = pd.DataFrame(rows[:-1], columns=header)
     42     all_dfs.append(df)
     43 

/srv/conda/lib/python3.6/site-packages/pandas/core/frame.py in __init__(self, data, index, columns, dtype, copy)
    433                     if is_named_tuple(data[0]) and columns is None:
    434                         columns = data[0]._fields
--> 435                     arrays, columns = to_arrays(data, columns, dtype=dtype)
    436                     columns = ensure_index(columns)
    437 

/srv/conda/lib/python3.6/site-packages/pandas/core/internals/construction.py in to_arrays(data, columns, coerce_float, dtype)
    402     if isinstance(data[0], (list, tuple)):
    403         return _list_to_arrays(data, columns, coerce_float=coerce_float,
--> 404                                dtype=dtype)
    405     elif isinstance(data[0], compat.Mapping):
    406         return _list_of_dict_to_arrays(data, columns,

/srv/conda/lib/python3.6/site-packages/pandas/core/internals/construction.py in _list_to_arrays(data, columns, coerce_float, dtype)
    434         content = list(lib.to_object_array(data).T)
    435     return _convert_object_array(content, columns, dtype=dtype,
--> 436                                  coerce_float=coerce_float)
    437 
    438 

/srv/conda/lib/python3.6/site-packages/pandas/core/internals/construction.py in _convert_object_array(content, columns, coerce_float, dtype)
    490             raise AssertionError('{col:d} columns passed, passed data had '
    491                                  '{con} columns'.format(col=len(columns),
--> 492                                                         con=len(content)))
    493 
    494     # provide soft conversion of object dtypes

AssertionError: 10 columns passed, passed data had 5 columns
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `doc/ntbk/z2jh/cost.py`
Content:
```
1 import numpy as np
2 from datetime import datetime as py_dtime
3 from datetime import timedelta
4 import pandas as pd
5 import requests
6 from bs4 import BeautifulSoup as bs4
7 
8 from bqplot import LinearScale, Axis, Lines, Figure, DateScale
9 from bqplot.interacts import HandDraw
10 from ipywidgets import widgets
11 from IPython.display import display
12 import locale
13 import warnings
14 warnings.filterwarnings('ignore')
15 locale.setlocale(locale.LC_ALL, '')
16 
17 # --- MACHINE COSTS ---
18 http = requests.get('https://cloud.google.com/compute/pricing')
19 http = bs4(http.text)
20 
21 # Munge the cost data
22 all_dfs = []
23 for table in http.find_all('table'):
24     header = table.find_all('th')
25     header = [item.text for item in header]
26 
27     data = table.find_all('tr')[1:]
28     rows = []
29     for ii in data:
30         thisrow = []
31         for jj in ii.find_all('td'):
32             if 'default' in jj.attrs.keys():
33                 thisrow.append(jj.attrs['default'])
34             elif 'ore-hourly' in jj.attrs.keys():
35                 thisrow.append(jj.attrs['ore-hourly'].strip('$'))
36             elif 'ore-monthly' in jj.attrs.keys():
37                 thisrow.append(jj.attrs['ore-monthly'].strip('$'))
38             else:
39                 thisrow.append(jj.text.strip())
40         rows.append(thisrow)
41     df = pd.DataFrame(rows[:-1], columns=header)
42     all_dfs.append(df)
43 
44 # Pull out our reference dataframes
45 disk = [df for df in all_dfs if 'Price (per GB / month)' in df.columns][0]
46 
47 machines_list = pd.concat([df for df in all_dfs if 'Machine type' in df.columns]).dropna()
48 machines_list = machines_list.drop('Preemptible price (USD)', axis=1)
49 machines_list = machines_list.rename(columns={'Price (USD)': 'Price (USD / hr)'})
50 active_machine = machines_list.iloc[0]
51 
52 # Base costs, all per day
53 disk['Price (per GB / month)'] = disk['Price (per GB / month)'].astype(float)
54 cost_storage_hdd = disk[disk['Type'] == 'Standard provisioned space']['Price (per GB / month)'].values[0]
55 cost_storage_hdd /= 30.  # To make it per day
56 cost_storage_ssd = disk[disk['Type'] == 'SSD provisioned space']['Price (per GB / month)'].values[0]
57 cost_storage_ssd /= 30.  # To make it per day
58 storage_cost = {False: 0, 'ssd': cost_storage_ssd, 'hdd': cost_storage_hdd}
59 
60 # --- WIDGET ---
61 date_start = py_dtime(2017, 1, 1, 0)
62 n_step_min = 2
63 
64 def autoscale(y, window_minutes=30, user_buffer=10):
65     # Weights for the autoscaling
66     weights = np.logspace(0, 2, window_minutes)[::-1]
67     weights /= np.sum(weights)
68 
69     y = np.hstack([np.zeros(window_minutes), y])
70     y_scaled = y.copy()
71     for ii in np.arange(window_minutes, len(y_scaled)):
72         window = y[ii:ii - window_minutes:-1]
73         window_mean = np.average(window, weights=weights)
74         y_scaled[ii] = window_mean + user_buffer
75     return y_scaled[window_minutes:]
76 
77 
78 def integrate_cost(machines, cost_per_day):
79     cost_per_minute = cost_per_day / (24. * 60. / n_step_min)  # 24 hrs * 60 min / N minutes per step
80     cost = np.nansum([ii * cost_per_minute for ii in machines])
81     return cost
82 
83 def calculate_machines_needed(users, mem_per_user, active_machine):
84     memory_per_machine = float(active_machine['Memory'].values[0].replace('GB', ''))
85     total_gigs_needed = [ii * mem_per_user for ii in users]
86     total_machines_needed = [int(np.ceil(ii / memory_per_machine)) for ii in total_gigs_needed]
87     return total_machines_needed
88 
89 def create_date_range(n_days):
90     delta = timedelta(n_days)
91     date_stop = date_start + delta
92     date_range = pd.date_range(date_start, date_stop, freq='{}min'.format(n_step_min))
93     return date_stop, date_range
94 
95 
96 def cost_display(n_days=7):
97 
98     users = widgets.IntText(value=8, description='Number of total users')
99     storage_per_user = widgets.IntText(value=10, description='Storage per user (GB)')
100     mem_per_user = widgets.IntText(value=2, description="RAM per user (GB)")
101     machines = widgets.Dropdown(description='Machine',
102                                 options=machines_list['Machine type'].values.tolist())
103     persistent = widgets.Dropdown(description="Persistent Storage?",
104                                   options={'HDD': 'hdd', 'SSD': 'ssd'},
105                                   value='hdd')
106     autoscaling = widgets.Checkbox(value=False, description='Autoscaling?')
107     text_avg_num_machine = widgets.Text(value='', description='Average # Machines:')
108     text_cost_machine = widgets.Text(value='', description='Machine Cost:')
109     text_cost_storage = widgets.Text(value='', description='Storage Cost:')
110     text_cost_total = widgets.Text(value='', description='Total Cost:')
111 
112     hr = widgets.HTML(value="---")
113 
114     # Define axes limits
115     y_max = 100.
116     date_stop, date_range = create_date_range(n_days)
117 
118     # Create axes and extra variables for the viz
119     xs_hd = DateScale(min=date_start, max=date_stop, )
120     ys_hd = LinearScale(min=0., max=y_max)
121 
122     # Shading for weekends
123     is_weekend = np.where([ii in [6, 7] for ii in date_range.dayofweek], 1, 0)
124     is_weekend = is_weekend * (float(y_max) + 50.)
125     is_weekend[is_weekend == 0] = -10
126     line_fill = Lines(x=date_range, y=is_weekend,
127                       scales={'x': xs_hd, 'y': ys_hd}, colors=['black'],
128                       fill_opacities=[.2], fill='bottom')
129 
130     # Set up hand draw widget
131     line_hd = Lines(x=date_range, y=10 * np.ones(len(date_range)),
132                     scales={'x': xs_hd, 'y': ys_hd}, colors=['#E46E2E'])
133     line_users = Lines(x=date_range, y=10 * np.ones(len(date_range)),
134                        scales={'x': xs_hd, 'y': ys_hd}, colors=['#e5e5e5'])
135     line_autoscale = Lines(x=date_range, y=10 * np.ones(len(date_range)),
136                            scales={'x': xs_hd, 'y': ys_hd}, colors=['#000000'])
137     handdraw = HandDraw(lines=line_hd)
138     xax = Axis(scale=xs_hd, label='Day', grid_lines='none',
139                tick_format='%b %d')
140     yax = Axis(scale=ys_hd, label='Numer of Users',
141                orientation='vertical', grid_lines='none')
142     # FIXME add `line_autoscale` when autoscale is enabled
143     fig = Figure(marks=[line_fill, line_hd, line_users],
144                  axes=[xax, yax], interaction=handdraw)
145 
146     def _update_cost(change):
147         # Pull values from the plot
148         max_users = max(handdraw.lines.y)
149         max_buffer = max_users * 1.05  # 5% buffer
150         line_users.y = [max_buffer] * len(handdraw.lines.y)
151         if max_users > users.value:
152             users.value = max_users
153 
154         autoscaled_users = autoscale(handdraw.lines.y)
155         line_autoscale.y = autoscaled_users
156 
157         # Calculate costs
158         active_machine = machines_list[machines_list['Machine type'] == machines.value]
159         machine_cost = active_machine['Price (USD / hr)'].values.astype(float) * 24  # To make it cost per day
160         users_for_cost = autoscaled_users if autoscaling.value is True else [max_buffer] * len(handdraw.lines.y)
161         num_machines = calculate_machines_needed(users_for_cost, mem_per_user.value, active_machine)
162         avg_num_machines = np.mean(num_machines)
163         cost_machine = integrate_cost(num_machines, machine_cost)
164         cost_storage = integrate_cost(num_machines, storage_cost[persistent.value] * storage_per_user.value)
165         cost_total = cost_machine + cost_storage
166 
167         # Set the values
168         for iwidget, icost in [(text_cost_machine, cost_machine),
169                                (text_cost_storage, cost_storage),
170                                (text_cost_total, cost_total),
171                                (text_avg_num_machine, avg_num_machines)]:
172             if iwidget is not text_avg_num_machine:
173                 icost = locale.currency(icost, grouping=True)
174             else:
175                 icost = '{:.2f}'.format(icost)
176             iwidget.value = icost
177 
178         # Set the color
179         if autoscaling.value is True:
180             line_autoscale.colors = ['#000000']
181             line_users.colors = ['#e5e5e5']
182         else:
183             line_autoscale.colors = ['#e5e5e5']
184             line_users.colors = ['#000000']
185 
186     line_hd.observe(_update_cost, names='y')
187     # autoscaling.observe(_update_cost)  # FIXME Uncomment when we implement autoscaling
188     persistent.observe(_update_cost)
189     machines.observe(_update_cost)
190     storage_per_user.observe(_update_cost)
191     mem_per_user.observe(_update_cost)
192 
193     # Show it
194     fig.title = 'Draw your usage pattern over time.'
195     # FIXME autoscaling when it's ready
196     display(users, machines, mem_per_user, storage_per_user, persistent, fig, hr,
197             text_cost_machine, text_avg_num_machine, text_cost_storage, text_cost_total)
198     return fig
199
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/doc/ntbk/z2jh/cost.py b/doc/ntbk/z2jh/cost.py
--- a/doc/ntbk/z2jh/cost.py
+++ b/doc/ntbk/z2jh/cost.py
@@ -15,16 +15,16 @@
 locale.setlocale(locale.LC_ALL, '')
 
 # --- MACHINE COSTS ---
-http = requests.get('https://cloud.google.com/compute/pricing')
-http = bs4(http.text)
+resp = requests.get('https://cloud.google.com/compute/pricing')
+html = bs4(resp.text)
 
 # Munge the cost data
 all_dfs = []
-for table in http.find_all('table'):
-    header = table.find_all('th')
+for table in html.find_all('table'):
+    header = table.find('thead').find_all('th')
     header = [item.text for item in header]
 
-    data = table.find_all('tr')[1:]
+    data = table.find('tbody').find_all('tr')
     rows = []
     for ii in data:
         thisrow = []

{"golden_diff": "diff --git a/doc/ntbk/z2jh/cost.py b/doc/ntbk/z2jh/cost.py\n--- a/doc/ntbk/z2jh/cost.py\n+++ b/doc/ntbk/z2jh/cost.py\n@@ -15,16 +15,16 @@\n locale.setlocale(locale.LC_ALL, '')\n \n # --- MACHINE COSTS ---\n-http = requests.get('https://cloud.google.com/compute/pricing')\n-http = bs4(http.text)\n+resp = requests.get('https://cloud.google.com/compute/pricing')\n+html = bs4(resp.text)\n \n # Munge the cost data\n all_dfs = []\n-for table in http.find_all('table'):\n-    header = table.find_all('th')\n+for table in html.find_all('table'):\n+    header = table.find('thead').find_all('th')\n     header = [item.text for item in header]\n \n-    data = table.find_all('tr')[1:]\n+    data = table.find('tbody').find_all('tr')\n     rows = []\n     for ii in data:\n         thisrow = []\n", "issue": "cost estimator fails to run\nTo reproduce:\r\n\r\n- Launch the cost estimator on binder https://mybinder.org/v2/gh/jupyterhub/zero-to-jupyterhub-k8s/master?filepath=doc/ntbk/draw_function.ipynb\r\n- Run the first cell\r\n- Observe a traceback similar to the following:\r\n\r\n```\r\n---------------------------------------------------------------------------\r\nAssertionError                            Traceback (most recent call last)\r\n<ipython-input-1-de8d1e48d121> in <module>\r\n----> 1 from z2jh import cost_display, disk, machines\r\n      2 machines\r\n\r\n~/doc/ntbk/z2jh/__init__.py in <module>\r\n      1 \"\"\"Helper functions to calculate cost.\"\"\"\r\n      2 \r\n----> 3 from .cost import cost_display, disk\r\n      4 from .cost import machines_list as machines\r\n\r\n~/doc/ntbk/z2jh/cost.py in <module>\r\n     39                 thisrow.append(jj.text.strip())\r\n     40         rows.append(thisrow)\r\n---> 41     df = pd.DataFrame(rows[:-1], columns=header)\r\n     42     all_dfs.append(df)\r\n     43 \r\n\r\n/srv/conda/lib/python3.6/site-packages/pandas/core/frame.py in __init__(self, data, index, columns, dtype, copy)\r\n    433                     if is_named_tuple(data[0]) and columns is None:\r\n    434                         columns = data[0]._fields\r\n--> 435                     arrays, columns = to_arrays(data, columns, dtype=dtype)\r\n    436                     columns = ensure_index(columns)\r\n    437 \r\n\r\n/srv/conda/lib/python3.6/site-packages/pandas/core/internals/construction.py in to_arrays(data, columns, coerce_float, dtype)\r\n    402     if isinstance(data[0], (list, tuple)):\r\n    403         return _list_to_arrays(data, columns, coerce_float=coerce_float,\r\n--> 404                                dtype=dtype)\r\n    405     elif isinstance(data[0], compat.Mapping):\r\n    406         return _list_of_dict_to_arrays(data, columns,\r\n\r\n/srv/conda/lib/python3.6/site-packages/pandas/core/internals/construction.py in _list_to_arrays(data, columns, coerce_float, dtype)\r\n    434         content = list(lib.to_object_array(data).T)\r\n    435     return _convert_object_array(content, columns, dtype=dtype,\r\n--> 436                                  coerce_float=coerce_float)\r\n    437 \r\n    438 \r\n\r\n/srv/conda/lib/python3.6/site-packages/pandas/core/internals/construction.py in _convert_object_array(content, columns, coerce_float, dtype)\r\n    490             raise AssertionError('{col:d} columns passed, passed data had '\r\n    491                                  '{con} columns'.format(col=len(columns),\r\n--> 492                                                         con=len(content)))\r\n    493 \r\n    494     # provide soft conversion of object dtypes\r\n\r\nAssertionError: 10 columns passed, passed data had 5 columns\r\n```\n", "before_files": [{"content": "import numpy as np\nfrom datetime import datetime as py_dtime\nfrom datetime import timedelta\nimport pandas as pd\nimport requests\nfrom bs4 import BeautifulSoup as bs4\n\nfrom bqplot import LinearScale, Axis, Lines, Figure, DateScale\nfrom bqplot.interacts import HandDraw\nfrom ipywidgets import widgets\nfrom IPython.display import display\nimport locale\nimport warnings\nwarnings.filterwarnings('ignore')\nlocale.setlocale(locale.LC_ALL, '')\n\n# --- MACHINE COSTS ---\nhttp = requests.get('https://cloud.google.com/compute/pricing')\nhttp = bs4(http.text)\n\n# Munge the cost data\nall_dfs = []\nfor table in http.find_all('table'):\n    header = table.find_all('th')\n    header = [item.text for item in header]\n\n    data = table.find_all('tr')[1:]\n    rows = []\n    for ii in data:\n        thisrow = []\n        for jj in ii.find_all('td'):\n            if 'default' in jj.attrs.keys():\n                thisrow.append(jj.attrs['default'])\n            elif 'ore-hourly' in jj.attrs.keys():\n                thisrow.append(jj.attrs['ore-hourly'].strip('$'))\n            elif 'ore-monthly' in jj.attrs.keys():\n                thisrow.append(jj.attrs['ore-monthly'].strip('$'))\n            else:\n                thisrow.append(jj.text.strip())\n        rows.append(thisrow)\n    df = pd.DataFrame(rows[:-1], columns=header)\n    all_dfs.append(df)\n\n# Pull out our reference dataframes\ndisk = [df for df in all_dfs if 'Price (per GB / month)' in df.columns][0]\n\nmachines_list = pd.concat([df for df in all_dfs if 'Machine type' in df.columns]).dropna()\nmachines_list = machines_list.drop('Preemptible price (USD)', axis=1)\nmachines_list = machines_list.rename(columns={'Price (USD)': 'Price (USD / hr)'})\nactive_machine = machines_list.iloc[0]\n\n# Base costs, all per day\ndisk['Price (per GB / month)'] = disk['Price (per GB / month)'].astype(float)\ncost_storage_hdd = disk[disk['Type'] == 'Standard provisioned space']['Price (per GB / month)'].values[0]\ncost_storage_hdd /= 30.  # To make it per day\ncost_storage_ssd = disk[disk['Type'] == 'SSD provisioned space']['Price (per GB / month)'].values[0]\ncost_storage_ssd /= 30.  # To make it per day\nstorage_cost = {False: 0, 'ssd': cost_storage_ssd, 'hdd': cost_storage_hdd}\n\n# --- WIDGET ---\ndate_start = py_dtime(2017, 1, 1, 0)\nn_step_min = 2\n\ndef autoscale(y, window_minutes=30, user_buffer=10):\n    # Weights for the autoscaling\n    weights = np.logspace(0, 2, window_minutes)[::-1]\n    weights /= np.sum(weights)\n\n    y = np.hstack([np.zeros(window_minutes), y])\n    y_scaled = y.copy()\n    for ii in np.arange(window_minutes, len(y_scaled)):\n        window = y[ii:ii - window_minutes:-1]\n        window_mean = np.average(window, weights=weights)\n        y_scaled[ii] = window_mean + user_buffer\n    return y_scaled[window_minutes:]\n\n\ndef integrate_cost(machines, cost_per_day):\n    cost_per_minute = cost_per_day / (24. * 60. / n_step_min)  # 24 hrs * 60 min / N minutes per step\n    cost = np.nansum([ii * cost_per_minute for ii in machines])\n    return cost\n\ndef calculate_machines_needed(users, mem_per_user, active_machine):\n    memory_per_machine = float(active_machine['Memory'].values[0].replace('GB', ''))\n    total_gigs_needed = [ii * mem_per_user for ii in users]\n    total_machines_needed = [int(np.ceil(ii / memory_per_machine)) for ii in total_gigs_needed]\n    return total_machines_needed\n\ndef create_date_range(n_days):\n    delta = timedelta(n_days)\n    date_stop = date_start + delta\n    date_range = pd.date_range(date_start, date_stop, freq='{}min'.format(n_step_min))\n    return date_stop, date_range\n\n\ndef cost_display(n_days=7):\n\n    users = widgets.IntText(value=8, description='Number of total users')\n    storage_per_user = widgets.IntText(value=10, description='Storage per user (GB)')\n    mem_per_user = widgets.IntText(value=2, description=\"RAM per user (GB)\")\n    machines = widgets.Dropdown(description='Machine',\n                                options=machines_list['Machine type'].values.tolist())\n    persistent = widgets.Dropdown(description=\"Persistent Storage?\",\n                                  options={'HDD': 'hdd', 'SSD': 'ssd'},\n                                  value='hdd')\n    autoscaling = widgets.Checkbox(value=False, description='Autoscaling?')\n    text_avg_num_machine = widgets.Text(value='', description='Average # Machines:')\n    text_cost_machine = widgets.Text(value='', description='Machine Cost:')\n    text_cost_storage = widgets.Text(value='', description='Storage Cost:')\n    text_cost_total = widgets.Text(value='', description='Total Cost:')\n\n    hr = widgets.HTML(value=\"---\")\n\n    # Define axes limits\n    y_max = 100.\n    date_stop, date_range = create_date_range(n_days)\n\n    # Create axes and extra variables for the viz\n    xs_hd = DateScale(min=date_start, max=date_stop, )\n    ys_hd = LinearScale(min=0., max=y_max)\n\n    # Shading for weekends\n    is_weekend = np.where([ii in [6, 7] for ii in date_range.dayofweek], 1, 0)\n    is_weekend = is_weekend * (float(y_max) + 50.)\n    is_weekend[is_weekend == 0] = -10\n    line_fill = Lines(x=date_range, y=is_weekend,\n                      scales={'x': xs_hd, 'y': ys_hd}, colors=['black'],\n                      fill_opacities=[.2], fill='bottom')\n\n    # Set up hand draw widget\n    line_hd = Lines(x=date_range, y=10 * np.ones(len(date_range)),\n                    scales={'x': xs_hd, 'y': ys_hd}, colors=['#E46E2E'])\n    line_users = Lines(x=date_range, y=10 * np.ones(len(date_range)),\n                       scales={'x': xs_hd, 'y': ys_hd}, colors=['#e5e5e5'])\n    line_autoscale = Lines(x=date_range, y=10 * np.ones(len(date_range)),\n                           scales={'x': xs_hd, 'y': ys_hd}, colors=['#000000'])\n    handdraw = HandDraw(lines=line_hd)\n    xax = Axis(scale=xs_hd, label='Day', grid_lines='none',\n               tick_format='%b %d')\n    yax = Axis(scale=ys_hd, label='Numer of Users',\n               orientation='vertical', grid_lines='none')\n    # FIXME add `line_autoscale` when autoscale is enabled\n    fig = Figure(marks=[line_fill, line_hd, line_users],\n                 axes=[xax, yax], interaction=handdraw)\n\n    def _update_cost(change):\n        # Pull values from the plot\n        max_users = max(handdraw.lines.y)\n        max_buffer = max_users * 1.05  # 5% buffer\n        line_users.y = [max_buffer] * len(handdraw.lines.y)\n        if max_users > users.value:\n            users.value = max_users\n\n        autoscaled_users = autoscale(handdraw.lines.y)\n        line_autoscale.y = autoscaled_users\n\n        # Calculate costs\n        active_machine = machines_list[machines_list['Machine type'] == machines.value]\n        machine_cost = active_machine['Price (USD / hr)'].values.astype(float) * 24  # To make it cost per day\n        users_for_cost = autoscaled_users if autoscaling.value is True else [max_buffer] * len(handdraw.lines.y)\n        num_machines = calculate_machines_needed(users_for_cost, mem_per_user.value, active_machine)\n        avg_num_machines = np.mean(num_machines)\n        cost_machine = integrate_cost(num_machines, machine_cost)\n        cost_storage = integrate_cost(num_machines, storage_cost[persistent.value] * storage_per_user.value)\n        cost_total = cost_machine + cost_storage\n\n        # Set the values\n        for iwidget, icost in [(text_cost_machine, cost_machine),\n                               (text_cost_storage, cost_storage),\n                               (text_cost_total, cost_total),\n                               (text_avg_num_machine, avg_num_machines)]:\n            if iwidget is not text_avg_num_machine:\n                icost = locale.currency(icost, grouping=True)\n            else:\n                icost = '{:.2f}'.format(icost)\n            iwidget.value = icost\n\n        # Set the color\n        if autoscaling.value is True:\n            line_autoscale.colors = ['#000000']\n            line_users.colors = ['#e5e5e5']\n        else:\n            line_autoscale.colors = ['#e5e5e5']\n            line_users.colors = ['#000000']\n\n    line_hd.observe(_update_cost, names='y')\n    # autoscaling.observe(_update_cost)  # FIXME Uncomment when we implement autoscaling\n    persistent.observe(_update_cost)\n    machines.observe(_update_cost)\n    storage_per_user.observe(_update_cost)\n    mem_per_user.observe(_update_cost)\n\n    # Show it\n    fig.title = 'Draw your usage pattern over time.'\n    # FIXME autoscaling when it's ready\n    display(users, machines, mem_per_user, storage_per_user, persistent, fig, hr,\n            text_cost_machine, text_avg_num_machine, text_cost_storage, text_cost_total)\n    return fig\n", "path": "doc/ntbk/z2jh/cost.py"}], "after_files": [{"content": "import numpy as np\nfrom datetime import datetime as py_dtime\nfrom datetime import timedelta\nimport pandas as pd\nimport requests\nfrom bs4 import BeautifulSoup as bs4\n\nfrom bqplot import LinearScale, Axis, Lines, Figure, DateScale\nfrom bqplot.interacts import HandDraw\nfrom ipywidgets import widgets\nfrom IPython.display import display\nimport locale\nimport warnings\nwarnings.filterwarnings('ignore')\nlocale.setlocale(locale.LC_ALL, '')\n\n# --- MACHINE COSTS ---\nresp = requests.get('https://cloud.google.com/compute/pricing')\nhtml = bs4(resp.text)\n\n# Munge the cost data\nall_dfs = []\nfor table in html.find_all('table'):\n    header = table.find('thead').find_all('th')\n    header = [item.text for item in header]\n\n    data = table.find('tbody').find_all('tr')\n    rows = []\n    for ii in data:\n        thisrow = []\n        for jj in ii.find_all('td'):\n            if 'default' in jj.attrs.keys():\n                thisrow.append(jj.attrs['default'])\n            elif 'ore-hourly' in jj.attrs.keys():\n                thisrow.append(jj.attrs['ore-hourly'].strip('$'))\n            elif 'ore-monthly' in jj.attrs.keys():\n                thisrow.append(jj.attrs['ore-monthly'].strip('$'))\n            else:\n                thisrow.append(jj.text.strip())\n        rows.append(thisrow)\n    df = pd.DataFrame(rows[:-1], columns=header)\n    all_dfs.append(df)\n\n# Pull out our reference dataframes\ndisk = [df for df in all_dfs if 'Price (per GB / month)' in df.columns][0]\n\nmachines_list = pd.concat([df for df in all_dfs if 'Machine type' in df.columns]).dropna()\nmachines_list = machines_list.drop('Preemptible price (USD)', axis=1)\nmachines_list = machines_list.rename(columns={'Price (USD)': 'Price (USD / hr)'})\nactive_machine = machines_list.iloc[0]\n\n# Base costs, all per day\ndisk['Price (per GB / month)'] = disk['Price (per GB / month)'].astype(float)\ncost_storage_hdd = disk[disk['Type'] == 'Standard provisioned space']['Price (per GB / month)'].values[0]\ncost_storage_hdd /= 30.  # To make it per day\ncost_storage_ssd = disk[disk['Type'] == 'SSD provisioned space']['Price (per GB / month)'].values[0]\ncost_storage_ssd /= 30.  # To make it per day\nstorage_cost = {False: 0, 'ssd': cost_storage_ssd, 'hdd': cost_storage_hdd}\n\n# --- WIDGET ---\ndate_start = py_dtime(2017, 1, 1, 0)\nn_step_min = 2\n\ndef autoscale(y, window_minutes=30, user_buffer=10):\n    # Weights for the autoscaling\n    weights = np.logspace(0, 2, window_minutes)[::-1]\n    weights /= np.sum(weights)\n\n    y = np.hstack([np.zeros(window_minutes), y])\n    y_scaled = y.copy()\n    for ii in np.arange(window_minutes, len(y_scaled)):\n        window = y[ii:ii - window_minutes:-1]\n        window_mean = np.average(window, weights=weights)\n        y_scaled[ii] = window_mean + user_buffer\n    return y_scaled[window_minutes:]\n\n\ndef integrate_cost(machines, cost_per_day):\n    cost_per_minute = cost_per_day / (24. * 60. / n_step_min)  # 24 hrs * 60 min / N minutes per step\n    cost = np.nansum([ii * cost_per_minute for ii in machines])\n    return cost\n\ndef calculate_machines_needed(users, mem_per_user, active_machine):\n    memory_per_machine = float(active_machine['Memory'].values[0].replace('GB', ''))\n    total_gigs_needed = [ii * mem_per_user for ii in users]\n    total_machines_needed = [int(np.ceil(ii / memory_per_machine)) for ii in total_gigs_needed]\n    return total_machines_needed\n\ndef create_date_range(n_days):\n    delta = timedelta(n_days)\n    date_stop = date_start + delta\n    date_range = pd.date_range(date_start, date_stop, freq='{}min'.format(n_step_min))\n    return date_stop, date_range\n\n\ndef cost_display(n_days=7):\n\n    users = widgets.IntText(value=8, description='Number of total users')\n    storage_per_user = widgets.IntText(value=10, description='Storage per user (GB)')\n    mem_per_user = widgets.IntText(value=2, description=\"RAM per user (GB)\")\n    machines = widgets.Dropdown(description='Machine',\n                                options=machines_list['Machine type'].values.tolist())\n    persistent = widgets.Dropdown(description=\"Persistent Storage?\",\n                                  options={'HDD': 'hdd', 'SSD': 'ssd'},\n                                  value='hdd')\n    autoscaling = widgets.Checkbox(value=False, description='Autoscaling?')\n    text_avg_num_machine = widgets.Text(value='', description='Average # Machines:')\n    text_cost_machine = widgets.Text(value='', description='Machine Cost:')\n    text_cost_storage = widgets.Text(value='', description='Storage Cost:')\n    text_cost_total = widgets.Text(value='', description='Total Cost:')\n\n    hr = widgets.HTML(value=\"---\")\n\n    # Define axes limits\n    y_max = 100.\n    date_stop, date_range = create_date_range(n_days)\n\n    # Create axes and extra variables for the viz\n    xs_hd = DateScale(min=date_start, max=date_stop, )\n    ys_hd = LinearScale(min=0., max=y_max)\n\n    # Shading for weekends\n    is_weekend = np.where([ii in [6, 7] for ii in date_range.dayofweek], 1, 0)\n    is_weekend = is_weekend * (float(y_max) + 50.)\n    is_weekend[is_weekend == 0] = -10\n    line_fill = Lines(x=date_range, y=is_weekend,\n                      scales={'x': xs_hd, 'y': ys_hd}, colors=['black'],\n                      fill_opacities=[.2], fill='bottom')\n\n    # Set up hand draw widget\n    line_hd = Lines(x=date_range, y=10 * np.ones(len(date_range)),\n                    scales={'x': xs_hd, 'y': ys_hd}, colors=['#E46E2E'])\n    line_users = Lines(x=date_range, y=10 * np.ones(len(date_range)),\n                       scales={'x': xs_hd, 'y': ys_hd}, colors=['#e5e5e5'])\n    line_autoscale = Lines(x=date_range, y=10 * np.ones(len(date_range)),\n                           scales={'x': xs_hd, 'y': ys_hd}, colors=['#000000'])\n    handdraw = HandDraw(lines=line_hd)\n    xax = Axis(scale=xs_hd, label='Day', grid_lines='none',\n               tick_format='%b %d')\n    yax = Axis(scale=ys_hd, label='Numer of Users',\n               orientation='vertical', grid_lines='none')\n    # FIXME add `line_autoscale` when autoscale is enabled\n    fig = Figure(marks=[line_fill, line_hd, line_users],\n                 axes=[xax, yax], interaction=handdraw)\n\n    def _update_cost(change):\n        # Pull values from the plot\n        max_users = max(handdraw.lines.y)\n        max_buffer = max_users * 1.05  # 5% buffer\n        line_users.y = [max_buffer] * len(handdraw.lines.y)\n        if max_users > users.value:\n            users.value = max_users\n\n        autoscaled_users = autoscale(handdraw.lines.y)\n        line_autoscale.y = autoscaled_users\n\n        # Calculate costs\n        active_machine = machines_list[machines_list['Machine type'] == machines.value]\n        machine_cost = active_machine['Price (USD / hr)'].values.astype(float) * 24  # To make it cost per day\n        users_for_cost = autoscaled_users if autoscaling.value is True else [max_buffer] * len(handdraw.lines.y)\n        num_machines = calculate_machines_needed(users_for_cost, mem_per_user.value, active_machine)\n        avg_num_machines = np.mean(num_machines)\n        cost_machine = integrate_cost(num_machines, machine_cost)\n        cost_storage = integrate_cost(num_machines, storage_cost[persistent.value] * storage_per_user.value)\n        cost_total = cost_machine + cost_storage\n\n        # Set the values\n        for iwidget, icost in [(text_cost_machine, cost_machine),\n                               (text_cost_storage, cost_storage),\n                               (text_cost_total, cost_total),\n                               (text_avg_num_machine, avg_num_machines)]:\n            if iwidget is not text_avg_num_machine:\n                icost = locale.currency(icost, grouping=True)\n            else:\n                icost = '{:.2f}'.format(icost)\n            iwidget.value = icost\n\n        # Set the color\n        if autoscaling.value is True:\n            line_autoscale.colors = ['#000000']\n            line_users.colors = ['#e5e5e5']\n        else:\n            line_autoscale.colors = ['#e5e5e5']\n            line_users.colors = ['#000000']\n\n    line_hd.observe(_update_cost, names='y')\n    # autoscaling.observe(_update_cost)  # FIXME Uncomment when we implement autoscaling\n    persistent.observe(_update_cost)\n    machines.observe(_update_cost)\n    storage_per_user.observe(_update_cost)\n    mem_per_user.observe(_update_cost)\n\n    # Show it\n    fig.title = 'Draw your usage pattern over time.'\n    # FIXME autoscaling when it's ready\n    display(users, machines, mem_per_user, storage_per_user, persistent, fig, hr,\n            text_cost_machine, text_avg_num_machine, text_cost_storage, text_cost_total)\n    return fig\n", "path": "doc/ntbk/z2jh/cost.py"}]}