initial sample course content

main
adam j hartz 7 months ago
commit 483400d3cb

@ -0,0 +1,293 @@
import os
import ast
import time
import uuid
import fcntl
import base64
import shutil
import signal
import string
import random
import tempfile
import threading
import traceback
import subprocess
always_rerender = True
# inherit things from the pythoncode qtype
tutor.qtype_inherit('pythoncode')
time = time.time
defaults['csq_show_skeleton'] = False
defaults['csq_extra_tests'] = []
defaults['csq_submission_filename'] = 'lab.py'
defaults['csq_npoints'] = 0
# we don't want the "Run Code" button from pythoncode
del checktext
del handle_check
def total_points(**info):
return info['csq_npoints']
# stupid little helper to safely close a file descriptor
def safe_close(fd):
try:
os.close(fd)
except:
pass
def render_html(last_log, **info):
name = info['csq_name']
init = last_log.get(name, (None, info['csq_initial']))
if isinstance(init, str):
fname = ''
else:
fname, init = init
params = {
'name': name,
'init': str(init),
'safeinit': (init or '').replace('<', '&lt;'),
'b64init': base64.b64encode(make_initial_display(info).encode()).decode(),
'dl': (' download="%s"' % info['csq_skeleton_name'])
if 'csq_skeleton_name' in info else 'download',
'dl2': (' download="%s"' % fname)
if 'csq_skeleton_name' in info else 'download',
}
out = ''
if last_log.get(name, None) is not None:
try:
fname, loc = last_log[name]
loc = os.path.basename(loc)
qstring = urlencode({'path': json.dumps(info['cs_path_info']),
'fname': loc})
out += '<br/>'
safe_fname = fname.replace('<', '').replace('>', '').replace('"', '').replace("'", '')
out += ('<a href="%s/cs_util/get_upload?%s" '
'download="%s">Download Your '
'Last Submission</a><br/>') % (info['cs_url_root'], qstring, safe_fname)
except:
pass
out += ('''<a href="%s/%s/lab_viewer?lab=%s&name=%s&as=%s" target="_blank"> '''
'''Click to View Your Last Submission</a><br />''') % (info['cs_url_root'], info['cs_course'], '/'.join(info['cs_path_info'][1:]),name,info['cs_username'])
out += '''<input type="file" style="display: none" id=%(name)s name="%(name)s" />''' % params
out += ('''<button class="btn btn-catsoop" id="%s_select_button">Select File</button>&nbsp;'''
'''<tt><span id="%s_selected_file">No file selected</span></tt>''') % (name, name)
out += ('''<script type="text/javascript">'''
'''$('#%s_select_button').click(function (){$("#%s").click();});'''
'''$('#%s').change(function (){$('#%s_selected_file').text($('#%s').val());});'''
'''</script>''') % (name, name, name, name, name)
return out
# helper to correctly write our strings
def pluralize(things):
l = len(things)
leader = 's ' if l > 1 else ' '
if l == 1:
rest = str(things[0])
elif l == 2:
rest = '%s and %s' % tuple(things)
else:
rest = '%s, and %s' % (', '.join(map(str, things[:-1])), things[-1])
return leader + rest
GET_TESTS_CODE = """
def get_tests(x):
if isinstance(x, unittest.suite.TestSuite):
out = []
for i in x._tests:
out.extend(get_tests(i))
return out
elif isinstance(x, list):
out = []
for i in x:
out.extend(get_tests(i))
return out
else:
return [x.id().split()[0]]
"""
# this is a little bit involved, but so much better with the new supporting
# infrastructure!
def handle_submission(submissions, **info):
try:
code = info['csm_loader'].get_file_data(info, submissions, info['csq_name'])
code = code.decode().replace('\r\n', '\n')
except:
return {'score': 0, 'msg': '<div class="bs-callout bs-callout-danger"><span class="text-danger"><b>Error:</b> Unable to decode the specified file. Is this the file you intended to upload?</span></div>'}
# okay... here we go...
# first grab the safe interpreter to use during checking
# and the location of the sandbox
sandbox_interpreter = info['csq_python_interpreter']
id_ = uuid.uuid4().hex
this_sandbox_location = os.path.join(info.get('csq_sandbox_dir', '/tmp/sandbox'), '009checks', id_)
# make sure the sandbox exists
shutil.rmtree(this_sandbox_location, True)
# now dump the files there.
# first, copy the regular files over (test cases, etc)
shutil.copytree(os.path.join(info['cs_data_root'], 'courses', *info['cs_path_info'], '_files'), this_sandbox_location)
# then save the user's code with the appropriate name
with open(os.path.join(this_sandbox_location, info['csq_submission_filename']), 'w') as f:
f.write(code)
# and put our modified test.py in place
magic = None
while magic is None or magic in code:
magic = ''.join(random.choice(string.ascii_letters) for _ in range(50))
test_filename = os.path.join(this_sandbox_location, 'test.py')
with open(test_filename) as f:
labtest = f.read()
labtest = labtest + '\n\nimport unittest.suite\n%s\nprint(%r, flush=True)\nr = res.result\nres.createTests()' % (GET_TESTS_CODE, magic)
labtest += '\n_tests = get_tests(res.test)\nprint(_tests, flush=True)'
names = ('errors', 'failures', 'skipped', 'unexpectedSuccesses')
labtest += '\n_failed = {i[0].id().split()[0] for i in sum([getattr(r, i, []) for i in %r], [])}' % (names, )
labtest += '\nprint([i for i in _tests if i not in _failed], flush=True)'
with open(os.path.join(this_sandbox_location, 'test.py'), 'w') as f:
f.write(labtest)
# at this point, everything should be in place. time to actually run the check.
if 'csq_tests_to_run' in info:
tests_to_run = info['csq_tests_to_run']
else:
tests_to_run = [{'args': [], 'timeout': info.get('csq_timeout', 2)}] # each test is a 2-tuple: arguments to be given to test.py, and the timeout for this test.
response = ''
overall_passed = []
overall_tests = []
for count, test in enumerate(tests_to_run):
test['args'] = list(map(str, test['args']))
# set up stdout and stderr
_o, outfname = tempfile.mkstemp()
_e, errfname = tempfile.mkstemp()
def _prep():
os.setpgrp()
info['csm_process'].set_pdeathsig()()
# run the test, keeping track of time
start = time.time()
proc = subprocess.Popen([sandbox_interpreter, 'test.py'] + test['args'],
cwd=this_sandbox_location,
bufsize=0,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
preexec_fn=_prep)
out = ''
err = ''
try:
out, err = proc.communicate(timeout=test['timeout'])
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
out, err = proc.communicate()
out = out.decode()
err = err.decode()
stop = time.time()
# hide a little bit of information in stack traces, just in case
out = out.replace(this_sandbox_location, 'TESTING_SANDBOX')
err = err.replace(this_sandbox_location, 'TESTING_SANDBOX')
# try to separate our logging output from regular output from the program
sout = out.rsplit(magic, 1)
failmsg = None
if len(sout) == 1:
# magic number didn't show up. we didn't reach the print.
# infinite loop / timeout?
alltests = []
passed = []
overall_tests = None
numtests = None
info['cs_debug']('FAILED2', info.get('cs_username', None), repr(out), repr(err))
elif len(sout) > 2:
# wtf? more than one magic number should not happen.
failmsg = 'There was an unidentified error when running this test.'
alltests = []
passed = []
overall_tests = None
numtests = None
else:
# this is where we hope to be.
# compute the effect of this test on the overall score
out, log = sout
res = log.strip().splitlines()
try:
alltests, passed = [ast.literal_eval(i) for i in res]
except:
failmsg = 'There was an unidentified error when running this test.'
alltests = []
passed = []
overall_tests = None
if overall_tests is not None:
overall_tests += alltests
numtests = len(alltests)
overall_passed += passed
# truncate the stdout if it is really long.
outlines = out.strip().splitlines()
out = '\n'.join(outlines)
if len(outlines) > 1000:
outlines = outlines[:1000] + ['...OUTPUT TRUNCATED...']
out = '\n'.join(outlines)
if len(out) > 10000:
out = out[:10000] + '\n...OUTPUT TRUNCATED...'
# how should the response be colored?
# green if everything passed; red otherwise
color = 'success' if alltests and set(alltests) == set(passed) else 'danger'
# now construct the HTML for this one test.
response += '<div class="bs-callout bs-callout-%s">' % color
response += '<p><b>Testing:</b> <code>python3 test.py%s</code> with a timeout of %.1f seconds</p>' % ((' ' if test['args'] else '') + ' '.join(test['args']), test['timeout'])
if color == 'danger':
response += '<b class="text-danger">Your code did not pass all test cases.</b><br/>'
if failmsg is not None:
response += failmsg
elif numtests is not None:
response += 'Your code passed %s of %s tests.<br/>' % (len(passed), len(alltests))
else:
response += 'Your code passed 0 tests.<br/>'
else:
if numtests == 1:
response += '<b class="text-success">Your code passed this test case.</b>'
else:
response += '<b class="text-success">Your code passed all %s test cases.</b>' % len(alltests)
response += '<p>Your code ran for %f seconds.' % (stop - start)
if proc.returncode == -9:
response += ' The process running your code did not run to completion. It was killed because it would have taken longer than %s second%s to run.' % (test['timeout'], '' if test['timeout'] == 1 else 's')
response += '</p>'
if out.strip() or err.strip():
response += '<p><button onclick="$(\'#%s_%d_results_showhide\').toggle()">Show/Hide Output</button>' % (magic, count)
response += '<div id="%s_%d_results_showhide" style="display:none">' % (magic, count)
if out.strip():
response += '<p>Your code produced the following output:</p>'
response += '<p><pre>%s</pre></p>' % out.replace('<','&lt;')
if err.strip():
response += '<p>This test produced the following output:</p>'
response += '<p><pre>%s</pre></p>' % err.replace('<','&lt;')
response += '</div>'
response += '</div>'
# all tests are done!
# clean up (delete all the files associated with this request)
shutil.rmtree(this_sandbox_location, True)
if overall_tests is not None:
response = '<h3>Test Results:</h3><p>Your code passed %d of %d tests.</p>%s' % (len(overall_passed), len(overall_tests), response)
else:
response = '<h3>Test Results:</h3><p>Your code passed %d tests.</p>%s' % (len(overall_passed), response)
return {'score': overall_passed, 'msg': response, 'extra_data': {'passed': overall_passed, 'run': overall_tests}}

@ -0,0 +1 @@
role='Admin'

@ -0,0 +1 @@
role = "Student"

@ -0,0 +1,11 @@
Hello! This sample course, which is designed to go over some of the basics of
CAT-SOOP, is set up to mirror the structure of a regular CAT-SOOP course.
This is the main page, which, for a normal class, might contain a calendar, or
weekly announcements, or links to assignments. The contents of this page are
contained in the `content.catsoop` in the root of the `sample_course`
directory. You should also take a look at `preload.py`, which controls much of
the behavior of this page.
Additionally, more detailed information is available from the "Pages" dropdown
at the top of this page.

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

@ -0,0 +1,60 @@
This page briefly describes some pieces of the input syntax for CAT-SOOP pages.
<section>Markdown</section>
Markdown works as expected, plus \$...\$ can be used for $\LaTeX$-style math.
* This is **bold**.
* This is _italicized_.
* This is `teletype`.
Some math:
$$-1 + \frac{1}{3} - \frac{1}{5} + \frac{1}{7} - \frac{1}{9} + \cdots = \sum_{n=1}^\infty \frac{\left(-1\right)^n}{2n - 1} = -\frac{\pi}{4}$$
Math is rendered with $\KaTeX$ (<https://github.com/Khan/KaTeX>) when possible,
and with MathJax (<https://www.mathjax.org/>) as a fallback.
<section>Python</section>
Python code can be included within <code>&lt;python&gt;</code> tags. Anything
that is printed within these tags will be rendered to the screen. For example,
the following is a random number generated by Python:
<python>
import random
print(random.randint(0, 20))
</python>
There is also a shorter syntax for including pre-computed values. `\@{...}`
can be used to print a single value. For example: @{random.randint(30, 70)}
It is also possible to specify how the value should be formatted. For example:
@%.02f{7}
<section>Images and Hyperlinks</section>
Images and hyperlinks can be included using regular HTML or Markdown syntax:
<a href="COURSE/markdown/stinkbug.png">Link</a>
![stinkbug](CURRENT/stinkbug.png)
In either case, the `src` or `href` can either be a regular URL, or it can be a
special form for use within CAT-SOOP. For locations starting with `CURRENT`,
CAT-SOOP will start looking in the current directory. For locations starting
with `COURSE`, CAT-SOOP will start looking the course's root directory.
These kinds of lookups first look for a regular CAT-SOOP page with the given
name. If one isn't found, CAT-SOOP will look in the corresponding `__STATIC__`
directory.
<section>Question</section>
One of the main uses of CAT-SOOP is to collect and assess responses to questions.
This is handled with the <code>&lt;question&gt;</code> tag, which is decribed
in more detail [on this page](COURSE/questions).
<question number>
csq_prompt='What is 2+2? '
csq_soln='4'
</question>

@ -0,0 +1 @@
cs_long_name = 'Markdown'

@ -0,0 +1,165 @@
# preload.py at each level defines special variables and/or functions to be
# inherited by pages farther down the tree.
# LOOK AND FEEL
cs_base_color = "#A31F34" # the base color
cs_header = '6.SAMP' # the upper-left corner
cs_icon_url = 'COURSE/favicon_local.gif' # the favicon, if any
# the 'header' text for the page
cs_long_name = cs_content_header = "Sample Course"
cs_title = 'Sample Course - CAT-SOOP' # the browser's title bar
# don't try to parse markdown inside of these tags
cs_markdown_ignore_tags = ('script', 'svg', 'textarea')
# defines the menu at the top of the page in the default template.
# each dictionary defines one menu item and should contain two keys:
# * text: the text to show for the link
# * link: the target of the link (either a URL or another list of this same form)
cs_top_menu = [
{'link': 'COURSE', 'text': 'Homepage'},
{'text': 'Pages', 'link': [
{'text': 'Structure', 'link': 'COURSE/structure'},
{'text': 'Markdown', 'link': 'COURSE/markdown'},
{'text': 'Questions', 'link': 'COURSE/questions'},
]},
# {'text': 'Sample Menu', 'link': [
# {'link': 'COURSE/calendar', 'text': 'Calendar and Handouts'},
# {'link': 'COURSE/announcements', 'text': 'Archived Announcements'},
# 'divider',
# {'link': 'COURSE/information', 'text': 'Basic Information'},
# {'link': 'COURSE/schedule_staff', 'text': 'Schedule and Staff'},
# {'link': 'COURSE/grading', 'text': 'Grading Policies'},
# {'link': 'COURSE/collaboration', 'text': 'Collaboration Policy'},
# ]},
# {'text': 'Piazza', 'link': 'https://piazza.com/mit/spring17/601'},
]
# AUTHENTICATION
cs_auth_type='login' # use the default (username/password based) authentication method
# for actually running a course at MIT, I like using OpenID Connect instead (https://oidc.mit.edu/).
# custom XML tag handling, copied from one i wrote for 6.01 ages ago. can
# probably largely be ignored (or can be updated to handle other kinds of
# tags).
import re
import hashlib
import subprocess
import shutil
def environment_matcher(tag):
return re.compile("""<%s>(?P<body>.*?)</%s>""" % (tag, tag), re.MULTILINE|re.DOTALL)
def cs_course_handle_custom_tags(text):
# CHECKOFFS AND CHECK YOURSELFS
checkoffs = 0
def docheckoff(match):
nonlocal checkoffs
d = match.groupdict()
checkoffs += 1
return '<div class="checkoff"><b>Checkoff %d:</b><p>%s</p><p><span id="queue_checkoff_%d"></span></p></div>' % (checkoffs, d['body'], checkoffs)
text=re.sub(environment_matcher('checkoff'), docheckoff, text)
checkyourself = 0
def docheckyourself(match):
nonlocal checkyourself
d = match.groupdict()
checkyourself += 1
return '<div class="question"><b>Check Yourself %d:</b><p>%s</p><p><span id="queue_checkyourself_%d"></span></p></div>' % (checkyourself, d['body'], checkyourself)
text=re.sub(environment_matcher('checkyourself'), docheckyourself, text)
return text
# PYTHON SANDBOX
csq_python3 = True
csq_python_sandbox = "python"
# something like the following can be used to use a sandboxed python
# interpreter on the production copy (after following the directions at
# https://catsoop.mit.edu/website/docs/installing/server_configuration for
# setting up the sandbox):
#
#if 'localhost' in cs_url_root:
# # locally, just use the system Python install
# csq_python_interpreter = '/usr/bin/python3'
#else:
# # on the server, use the properly sandboxed python
# csq_python_interpreter = '/home/ubuntu/py3_sandbox/bin/python3'
# PERMISSIONS
# users' roles are determined by the files in the __USERS__ directory. each
# has the form username.py other information (such as a section number, if
# relevant) can be stored there as well but the system will look for role =
# "Student" or similar, and use that to set the user's permissions.
#
# view: allowed to view the contents of a page
# submit: allowed to submit to a page
# view_all: always allowed to view every page, regardless of when it releases
# submit_all: always allowed to submit to every question, regardless of when it releases or is due
# impersonate: allowed to view the page "as" someone else
# admin: administrative tasks (such as modifying group assignments)
# whdw: allowed to see "WHDW" page (Who Has Done What)
# email: allowed to send e-mail through CAT-SOOP
# grade: allowed to submit grades
cs_default_role = 'Guest'
cs_permissions = {'Admin': ['view_all', 'submit_all', 'impersonate', 'admin', 'whdw', 'email', 'grade'],
'Instructor': ['view_all', 'submit_all', 'impersonate', 'admin', 'whdw', 'email', 'grade'],
'TA': ['view_all', 'submit_all', 'impersonate', 'whdw', 'email', 'grade'],
'UTA': ['view_all', 'submit_all', 'impersonate', 'grade'],
'LA': ['view_all', 'submit_all','impersonate', 'grade'],
'Student': ['view', 'submit'],
'Guest': ['view']}
# TIMING
# release and due dates can always be specified in absolute terms:
# "YYYY-MM-DD:HH:MM"
# the following allows the use of relative times (and/or per-section times)
# section_times maps section names to times (below, the default section has
# lecture at 8am on Tuesdays)
# this allows setting, e.g., cs_release_date = "lec:2" to mean "release this
# at lecture time in week 2"
# different sections will get different times if they are specified below.
# adding section = 2, for example, to someone's __USERS__ file will cause the
# system to look up the key 2 in the dictionary below.
cs_first_monday = '2017-02-06:00:00'
section_times = {'default': {'lec': 'T:08:00', 'lab': 'T:09:00', 'lab_due':'M+:22:00', 'soln':'S+:08:00', 'tut':'W:08:00'},
}
def cs_realize_time(meta, rel):
try:
start, end = rel.split(':')
section = cs_user_info.get('section', 'default')
rel = section_times.get(section, {}).get(start, section_times['default'].get(start, 'NEVER'))
meta['cs_week_number'] = int(end)
except:
pass
return csm_time.realize_time(meta,rel)
import time
from datetime import datetime
# cs_post_load is invoked after the page is loaded but before it is rendered.
# the example below shows the time at which the current page was last modified
# (based on the Git history).
def cs_post_load(context):
if 'cs_long_name' in context:
context['cs_content_header'] = context['cs_long_name']
context['cs_title'] = '%s | %s' % (context['cs_long_name'], context['cs_title'])
try:
loc = os.path.abspath(os.path.join(context['cs_data_root'], 'courses', *context['cs_path_info']))
git_info = subprocess.check_output(["git", "log", "--pretty=format:%h %ct",
"-n1", "--", "content.catsoop",
"content.md", "content.xml",
"content.py"], cwd=loc)
h, t = git_info.split()
t = context['csm_time'].long_timestamp(datetime.fromtimestamp(float(t))).replace(';', ' at')
context['cs_footer'] = 'This page was last updated on %s (revision <code>%s</code>).<br/>&nbsp;<br/>' % (t, h.decode())
except:
pass

@ -0,0 +1,305 @@
This page describes some of the different kinds of questions that can be asked
using things built in to CAT-SOOP. I'd also like, eventually, to expand this
page to include some discussion of creating custom question types.
Questions are always specified using the <code>&lt;question&gt;</code> tag.
Each question has a _type_ (which governs how the question is displayed, how
the answers are checked, etc), as well as some options that are specific to
that type of question. The options are provided as Python code inside the tag.
Right now, there aren't a lot of words on this page. But there are examples of
all the built-in question types. Look at the source of this page
(`sample_course/questions/content.catsoop`) to see how it was generated.
Note that you will need the `'submit'` permission to submit answers to these
questions.
<section>Options Present in All Questions</section>
* `csq_prompt`: A piece of text to be displayed before the question's input.
* `csq_name`: A unique identifier (alphanumeric only) for the question. If none is provided, a default value is used.
* `csq_npoints`: The number of points the question should be worth.
* `csq_check_function`: A function of two arguments (submitted value and expected value). If none is specified, a sane default function for the given question type is used. The return value must be one of:
* A Boolean representing whether the answer was correct.
* A single number between 0 and 1 representing a score.
* A tuple/list of two elements. The first is a score, and the second is a string containing a message to be displayed to the user.
* A dictionary mapping the keys `'score'` and `'msg'` to a score and a message, respectively.
* `csq_explanation`: An optional piece of text describing why the answer is what it is. After a student views the answer, they are given the option to view the explanation as well.
* `csq_grading_mode`: How the question should be graded. The default value is `'auto'`. The available modes are:
* `'auto'`: Send the response to the asynchronous auto-grader to be checked. Nice in particular for checks that run for a long time, to prevent them eating up the web server's resources.
* `'legacy'`: Check the response in the same request that submits it. Nice for short checks.
* `'manual'`: No automatic score is assigned. Rather, the submission is logged so it can later be manually graded.
<section>Base Question Types</section>
<subsection>smallbox</subsection>
A small box accepting arbitrary text input.
<question smallbox>
csq_prompt = 'Type the word "cat", with any capitalization: '
csq_soln = 'CAT'
csq_check_function = lambda sub, sol: sub.lower().strip() == sol.lower()
csq_size = 30 # width of text box
</question>
<subsection>bigbox</subsection>
A bigger box accepting arbitrary text input. Often used for survey responses.
<question bigbox>
csq_prompt = 'Type an essay detailing your experiences. Any answer receives full credit.<br/>'
csq_soln = 'CAT' # not necessary
csq_rows = 5
csq_cols = 50
csq_check_function = lambda sub, sol: 1 # check functions two args
</question>
<subsection>multiplechoice</subsection>
Multiple choice questions come in a few different forms, based on the value of `csq_renderer`. The default mode renders choices as a drop-down menu:
<question multiplechoice>
csq_prompt='What color is an orange?'
csq_options=['red', 'orange', 'yellow', 'green', 'blue', 'purple']
csq_soln = 'orange'
csq_explanation = '''While some oranges may be slightly different colors
(particularly if they are not ripe), most oranges are, in fact, orange.'''
#
</question>
For complicated options (that use math or other formatting, for example), the options can be rendered as radio buttons:
<question multiplechoice>
csq_renderer = 'radio'
csq_prompt='Which of the following is right?'
csq_options=['$x^2$', r'$\log\left(x\right)$', r'$\frac{x}{2}$']
csq_soln = 0
csq_soln_mode = 'index' # (otherwies csq_soln = csq_options[0])
</question>
The forms above are all "multiple choice, single answer." For "multiple choice, multiplce answer" questions, you can use the `'checkbox'` renderer.
<question multiplechoice>
csq_renderer = 'checkbox'
csq_prompt='Which of the following are valid ways to greet someone? Check all that apply.'
csq_options=['Aloha', 'Guten Tag', 'Auf Wiedersehen', 'Hey', 'Goodbye', 'Tschüß']
csq_soln = [True, True, False, True, False, False]
</question>
<section>Python-related Questions</section>
<subsection>pythonic</subsection>
`pythonic` questions expect a single Python expression as input, and a single
Python expression as output.
<question pythonic>
csq_prompt = 'Enter a list of at least 5 Python numbers, all distinct and all in the range $(0, 0.5]$:<br/>'
csq_soln = [0.01, 0.1, 0.2, 0.3, 0.5]
def csq_check_function(sub, sol):
if not isinstance(sub, list):
return 0, 'Please enter a Python list.'
if not all(0 < i <= 0.5 for i in sub):
return 0, 'All of the numbers must be in the range (0, 0.5].'
if len(sub) != len(set(sub)):
return 0, 'All the elements must be distinct.'
if len(sub) < 5:
return 0, 'Your list must contain at least 5 numbers.'
return 1
</question>
<subsection>pythonliteral</subsection>
`pythonliteral` is like `pythoncode`, except that it expects a single literal
value instead of an abitrary expression. This can be useful for numbers.
<question pythonliteral>
csq_prompt = 'What is $\sqrt{7}$, accurate to at least 3 decimal places? Entering `7**0.5` won\'t work!<br/>'
csq_soln = 7**0.5
csq_check_function = lambda sub, sol: abs(sub - sol) <= 1e-3
</question>
<subsection>pythoncode</subsection>
For larger coding problems (e.g., writing a function or computing a value), the `pythoncode` question
should be used. It is a little more complicated than the other question types.
Its default mode of operation compares the result from submitted code against
the result of "exemplar" code with the same input. Test cases are specified via
the option `csq_tests`.
There are multiple options for theinterface students see, controlled by the `csq_interface` option:
* `'textarea'`: A plain HTML box to paste code into.
* `'ace'`: A nicer input box, with syntax highlighting.
* `'upload'`: A file upload box.
Here is a minimal example:
<python>
PRIMES = [127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181,
191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263,
269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349,
353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433,
439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521,
523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613,
617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701,
709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809,
811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887,
907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997,
1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063,
1069, 1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151,
1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229,
1231, 1237, 1249, 1259, 1277, 1279, 1283, 1289, 1291, 1297, 1301,
1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, 1381, 1399, 1409,
1423, 1427, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, 1481,
1483, 1487, 1489, 1493, 1499, 1511, 1523, 1531, 1543, 1549, 1553,
1559, 1567, 1571, 1579, 1583, 1597, 1601, 1607, 1609, 1613, 1619,
1621, 1627, 1637, 1657, 1663, 1667, 1669, 1693, 1697, 1699, 1709,
1721, 1723, 1733, 1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789,
1801, 1811, 1823, 1831, 1847, 1861, 1867, 1871, 1873, 1877, 1879,
1889, 1901, 1907, 1913, 1931, 1933, 1949, 1951, 1973, 1979, 1987,
1993, 1997, 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063,
2069, 2081, 2083, 2087, 2089, 2099, 2111, 2113, 2129, 2131, 2137,
2141, 2143, 2153, 2161, 2179, 2203, 2207, 2213, 2221, 2237, 2239,
2243, 2251, 2267, 2269, 2273, 2281, 2287, 2293, 2297, 2309, 2311,
2333, 2339, 2341, 2347, 2351, 2357, 2371, 2377, 2381, 2383, 2389,
2393, 2399, 2411, 2417, 2423, 2437, 2441, 2447, 2459, 2467, 2473,
2477, 2503, 2521, 2531, 2539, 2543, 2549, 2551, 2557, 2579, 2591,
2593, 2609, 2617, 2621, 2633, 2647, 2657, 2659, 2663, 2671, 2677,
2683, 2687, 2689, 2693, 2699, 2707, 2711, 2713, 2719, 2729, 2731,
2741, 2749, 2753, 2767, 2777, 2789, 2791, 2797, 2801, 2803, 2819,
2833, 2837, 2843, 2851, 2857, 2861, 2879, 2887, 2897, 2903, 2909,
2917, 2927, 2939, 2953, 2957, 2963, 2969, 2971, 2999, 3001, 3011,
3019, 3023, 3037, 3041, 3049, 3061, 3067, 3079, 3083, 3089, 3109,
3119, 3121, 3137, 3163, 3167, 3169, 3181, 3187, 3191, 3203, 3209,
3217, 3221, 3229, 3251, 3253, 3257, 3259, 3271, 3299, 3301, 3307,
3313, 3319, 3323, 3329, 3331, 3343, 3347, 3359, 3361, 3371, 3373,
3389, 3391, 3407, 3413, 3433, 3449, 3457, 3461, 3463, 3467, 3469,
3491, 3499, 3511, 3517, 3527, 3529, 3533, 3539, 3541, 3547, 3557,
3559, 3571, 3581, 3583, 3593, 3607, 3613, 3617, 3623, 3631, 3637,
3643, 3659, 3671, 3673, 3677, 3691, 3697, 3701, 3709, 3719, 3727,
3733, 3739, 3761, 3767, 3769, 3779, 3793, 3797, 3803, 3821, 3823,
3833, 3847, 3851, 3853, 3863, 3877, 3881, 3889, 3907, 3911, 3917,
3919, 3923, 3929, 3931, 3943, 3947, 3967, 3989, 4001, 4003, 4007,
4013, 4019, 4021, 4027, 4049, 4051, 4057, 4073, 4079, 4091, 4093,
4099, 4111, 4127, 4129, 4133, 4139, 4153, 4157, 4159, 4177, 4201,
4211, 4217, 4219, 4229, 4231, 4241, 4243, 4253, 4259, 4261, 4271,
4273, 4283, 4289, 4297, 4327, 4337, 4339, 4349, 4357, 4363, 4373,
4391, 4397, 4409, 4421, 4423, 4441, 4447, 4451, 4457, 4463, 4481,
4483, 4493, 4507, 4513, 4517, 4519, 4523, 4547, 4549, 4561, 4567,
4583, 4591, 4597, 4603, 4621, 4637, 4639, 4643, 4649, 4651, 4657,
4663, 4673, 4679, 4691, 4703, 4721, 4723, 4729, 4733, 4751, 4759,
4783, 4787, 4789, 4793, 4799, 4801, 4813, 4817, 4831, 4861, 4871,
4877, 4889, 4903, 4909, 4919, 4931, 4933, 4937, 4943, 4951, 4957,
4967, 4969, 4973, 4987, 4993, 4999, 5003, 5009, 5011, 5021, 5023,
5039, 5051, 5059, 5077, 5081, 5087, 5099, 5101, 5107, 5113, 5119,
5147, 5153, 5167, 5171, 5179, 5189, 5197, 5209, 5227, 5231, 5233,
5237, 5261, 5273, 5279, 5281, 5297, 5303, 5309, 5323, 5333, 5347,
5351, 5381, 5387, 5393, 5399, 5407, 5413, 5417, 5419, 5431, 5437,
5441, 5443, 5449, 5471, 5477, 5479, 5483, 5501, 5503, 5507, 5519,
5521, 5527, 5531, 5557, 5563, 5569, 5573, 5581, 5591, 5623, 5639,
5641, 5647, 5651, 5653, 5657, 5659, 5669, 5683, 5689, 5693, 5701,
5711, 5717, 5737, 5741, 5743, 5749, 5779, 5783, 5791, 5801, 5807,
5813, 5821, 5827, 5839, 5843, 5849, 5851, 5857, 5861, 5867, 5869,
5879, 5881, 5897, 5903, 5923, 5927, 5939, 5953, 5981, 5987, 6007,
6011, 6029, 6037, 6043, 6047, 6053, 6067, 6073, 6079, 6089, 6091,
6101, 6113, 6121, 6131, 6133, 6143, 6151, 6163, 6173, 6197, 6199,
6203, 6211, 6217, 6221, 6229, 6247, 6257, 6263, 6269, 6271, 6277,
6287, 6299, 6301, 6311, 6317, 6323, 6329, 6337, 6343, 6353, 6359,
6361, 6367, 6373, 6379, 6389, 6397, 6421, 6427, 6449, 6451, 6469,
6473, 6481, 6491, 6521, 6529, 6547, 6551, 6553, 6563, 6569, 6571,
6577, 6581, 6599, 6607, 6619, 6637, 6653, 6659, 6661, 6673, 6679,
6689, 6691, 6701, 6703, 6709, 6719, 6733, 6737, 6761, 6763, 6779,
6781, 6791, 6793, 6803, 6823, 6827, 6829, 6833, 6841, 6857, 6863,
6869, 6871, 6883, 6899, 6907, 6911, 6917, 6947, 6949, 6959, 6961,
6967, 6971, 6977, 6983, 6991, 6997, 7001, 7013, 7019, 7027, 7039,
7043, 7057, 7069, 7079, 7103, 7109, 7121, 7127, 7129, 7151, 7159,
7177, 7187, 7193, 7207, 7211, 7213, 7219, 7229, 7237, 7243, 7247,
7253, 7283, 7297, 7307, 7309, 7321, 7331, 7333, 7349, 7351, 7369,
7393, 7411, 7417, 7433, 7451, 7457, 7459, 7477, 7481, 7487, 7489,
7499, 7507, 7517, 7523, 7529, 7537, 7541, 7547, 7549, 7559, 7561,
7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621, 7639, 7643, 7649,
7669, 7673, 7681, 7687, 7691, 7699, 7703, 7717, 7723, 7727, 7741,
7753, 7757, 7759, 7789, 7793, 7817, 7823, 7829, 7841, 7853, 7867,
7873, 7877, 7879, 7883, 7901, 7907, 7919]
</python>
<question pythoncode>
csq_interface = 'ace'
csq_prompt="""
Define a function `prime` that takes a single integer argument
and returns `True` if the argument is prime, and `False`
otherwise.
You may assume that the function's argument is a nonnegative integer.
0 and 1 should _not_ be considered prime numbers.
"""
csq_initial="""def prime(x):
pass # your code here!"""
csq_soln="""def prime(x):
if x == 0 or x == 1:
return False
for i in range(2,int(x/2+1)):
if x%i == 0:
return False
return True"""
csq_tests = [{'code':'ans = prime(997)'},
{'code':'ans = [(i,prime(i)) for i in range(100)]'},
] + [
{'code':'ans = prime(%s)' % cs_random.choice(PRIMES)} for i in range(4)
] + [
{'code':'ans = prime(%s)' % cs_random.randint(5000,10000)} for i in range(4)
]
</question>
```py
def fib(n):
pass # comment
```
For a bigger example from 6.145, see the following page (which requires the
`matplotlib` package):
* [Linear Regression](CURRENT/linregress)
<section>Symbolic Expressions</section>
<subsection>expression</subsection>
This question type is used for checking symolic math expressions.
<question expression>
csq_prompt = 'What is the square root of $\cos\omega + j\sin\omega$?<br/>'
csq_syntax='python'
csq_soln = ['sqrt(e**(j*omega))', 'e**(j*omega / 2)'] # can either be a single string, or a list of multiple correct answers.
</question>
<section>Per-User Randomized Values</section>
Invoking the `csm_tutor.init_random()` function will seed the `random.Random`
object stored in `cs_random` in such a way that the values it produces will be
consistent when the same user revisits a page. This is an easy way to add some
small variation to problems. For example, the following code will show
different numbers to everyone who visits to page:
<python>
# calling init_random() makes random numbers on the page always be the same for
# a given user
csm_tutor.init_random()
number_1 = cs_random.randint(1,9)
number_2 = cs_random.randint(21,29)
tsum = number_1 + number_2
</python>
<question pythonic>
csq_prompt = 'What is the sum of $@{number_1}$ and $@{number_2}$?<br/>'
csq_soln = tsum
csq_check_function = lambda submission, solution: abs(submission-solution) <= 1e-6
</question>

@ -0,0 +1,366 @@
<python>
cs_content_header = cs_long_name
def sample_mean(xs):
return sum(xs) / len(xs)
def sample_stddev(xs):
m = sample_mean(xs)
return (sum([(i-m)**2 for i in xs]) / (len(xs)-1))**0.5
def sample_cov(xs, ys):
x_mean = sample_mean(xs)
y_mean = sample_mean(ys)
o = 0.0
for index in range(len(xs)):
o += (xs[index] - x_mean)*(ys[index] - y_mean)
return o / (len(xs) - 1)
def pearson_r(xs, ys):
return sample_cov(xs, ys) / (sample_stddev(xs)*sample_stddev(ys))
def lin_regress_2(xs, ys):
r = pearson_r(xs, ys)
m = r * sample_stddev(ys) / sample_stddev(xs)
b = sample_mean(ys) - m * sample_mean(xs)
return m, b, r
#xs and ys are defined in preload.py
m, b, r = lin_regress_2(xs, ys)
import base64
# see preload.py for definition of the PlotWindow class.
def make_regress_plot(data_x, data_y, m, b):
fig = PlotWindow()
fig.scatter(data_x, data_y)
fig.plot([min(data_x), max(data_x)], [m*min(data_x)+b, m*max(data_x)+b], 'r')
return fig._show(width='400')
fig = PlotWindow()
fig.scatter(xs, ys)
img1 = fig._show(width='400')
</python>
<center><a href="https://www.youtube.com/watch?v=8qjl4lysi_s" target="_blank">Music for this Problem</a></center>
<section>Introduction</section>
In this exercise, we will write a program to compute a 2-dimensional _linear
regression_ for a set of data. In short, we will try to explain the
relationship between an independent variable $X$ and a single dependent
variable $Y$ based on a set of $N$ experimental data points $[(x_0, y_0), (x_1,
y_1), \ldots, (x_{N-1}, y_{N-1})]$.
In particular, we will try to find the line $y = mx + b$ that best approximates
the relationship represented in the data. In addition, we will calculate
[Pearson's r](https://en.wikipedia.org/wiki/Pearson_correlation_coefficient)
for the variables, which will tell us how strongly the two variables are
correlated.
Here, we will consider the "best fit" line to be the one that minimizes the sum
of squared errors between the sampled $y$ values and the values predicted by
the line:
$$\underset{m, b}{\text{argmin}} \sum_{i=0}^{N-1}\left(y_i - (mx_i + b)\right)^2$$
For example, here is a set of data:
<center>
@{img1}
</center>
And here is the line that best approximates the relationship between the two variables:
<center>
@{make_regress_plot(xs, ys, m, b)}
</center>
Next week, we will look at _using_ linear regression to solve some authentic
problems, but for now we'll just focus on _implementing_ a linear regression
(i.e., writing a program to _find_ the line, given a set of data).
As you might guess, this will be a fairly complicated program, and so, as we
have seen before, we will break it down into smaller pieces first.
<section>Primitives</section>
We'll start by defining some primitives:
<subsection>Sample Mean</subsection>
The _sample mean_ of a set of $N$ values $x_0, x_1, \ldots, x_{N-1}$ is given by:
$$\overline{X} = \frac{1}{N}\sum_{i=0}^{N-1} x_i$$
<subsection>Sample Variance</subsection>
The _sample variance_ of a set of $N$ values $x_0, x_1, \ldots, x_{N-1}$ is given by the following (note the two separate forms):
$$\text{Var}(X) = \frac{1}{N-1}\sum_{i=0}^{N-1} (x_i - \overline{X})^2$$
$$\text{Var}(X) = \frac{1}{N-1}\left(\sum_{i=0}^{N-1}(x_i^2) - \frac{1}{N}\left(\sum_{i=0}^{N-1}x_i\right)^2\right)$$
<subsection>Sample Standard Deviation</subsection>
The _sample standard deviation_ of a set of $N$ values $x_0, x_1, \ldots, x_{N-1}$ is given by:
$$\sigma_X = \sqrt{\text{Var}(X)} = \sqrt{\frac{1}{N-1}\sum_{i=0}^{N-1} (x_i - \overline{X})^2}$$
<subsection>Sample Covariance</subsection>
The _sample covariance_ of two variables $X$ and $Y$ (an estimate of the true underlying covariance of the two variables), each with $N$ samples, such that $y_i$ is associated with $x_i$, is given by the following (note the two separate forms):
$$\text{cov}(X, Y) = \frac{1}{N-1}\sum_{i=0}^{N-1} (x_i-\overline{X})(y_i-\overline{Y})$$
$$\text{cov}(X, Y) = \frac{1}{N-1}\left(\sum_{i=0}^{N-1}(x_iy_i) - \frac{1}{N}\left(\sum_{i=0}^{N-1}x_i\right)\left(\sum_{i=0}^{N-1}y_i\right)\right)$$
<section>Finding the "Best Fit" Line</section>
We would like to find the values $m$ and $b$ for which $y = mx+b$ best approximates the data we've gathered.
Earlier, we said that the line we wanted to find was defined by the values of
$m$ and $b$ that minimized the sum of squared errors between our sampled values
and the values predicted by the line $y=mx+b$:
$$\underset{m, b}{\text{argmin}} \sum_{i=0}^{N-1}\left(y_i - (mx_i + b)\right)^2$$
It is possible to solve for these values, and we will use them our
implementation. The next section shows a derivation of the solutions for $m$
and $b$. Understanding the derivation is not crucial for this exercise,
however, and so you are welcome to skip the derivation and move on to
<ref label="results"><a href="{link}">{type} {number}: {title}</a></ref>.
<subsection>Derivation</subsection>
Here, we want to find the values of $m$ and $b$ that minimize the sum of squared error (SSE):
$$\text{SSE} = \sum_{i=0}^{N-1}\left(y_i - (mx_i + b)\right)^2$$
We can use calculus for this: we take the partial derivatives of this
expression with respect to $m$ and $b$, respectively; and set each to 0 (the
function has a minimum where its derivative is 0).
$$\frac{\delta}{\delta b}\text{SSE} = \sum_{i=0}^{N-1} -2(y_i - mx_i - b)$$
$$\frac{\delta}{\delta m}\text{SSE} = \sum_{i=0}^{N-1} -2x_i(y_i - mx_i - b)$$
<subsubsection>Solving for b</subsubsection>
Let's start by considering the partial derivative with respect to $b$. We want to find when this expression is 0.
$$\sum_{i=0}^{N-1} -2(y_i - mx_i - b) = 0$$
We can start by pulling the constant ($-2$) in front of the summation, and dividing both sides by $-2$:
$$-2 \sum_{i=0}^{N-1} (y_i - mx_i - b) = 0$$
$$\sum_{i=0}^{N-1} (y_i - mx_i - b) = 0$$
And distributing the summation operator:
$$\sum_{i=0}^{N-1} y_i - \sum_{i=0}^{N-1}mx_i - \sum_{i=0}^{N-1}b = 0$$
We can pull the "$m$" and "$b$" constants out of their respective sums:
$$\sum_{i=0}^{N-1} y_i - m\sum_{i=0}^{N-1}x_i - b\sum_{i=0}^{N-1}1 = 0$$
Then we can isolate the "$b$" term by moving it to the right-hand side of the equation:
$$\sum_{i=0}^{N-1} y_i - m\sum_{i=0}^{N-1}x_i = b\sum_{i=0}^{N-1}1$$
And we can evaluate that sum:
$$\sum_{i=0}^{N-1} y_i - m\sum_{i=0}^{N-1}x_i = Nb$$
Then, to solve for $b$, we can divide both sides by $N$:
$$\frac{1}{N}\sum_{i=0}^{N-1} y_i - m\left(\frac{1}{N}\sum_{i=0}^{N-1}x_i\right) = b$$
Notice that the two expressions on the left have the form of our _sample mean_ equation from above, so we know that this must be:
$$\overline{Y} - m\overline{X} = b$$
<subsubsection>Solving for m</subsubsection>
Now we'll consider the partial derivative with respect to $m$, and again set it equal to zero.
$$\sum_{i=0}^{N-1} -2x_i(y_i - mx_i - b) = 0$$
Then we can pull the constant $-2$ out in front of the sum and distribute the $x_i$ across the addition:
$$-2 \sum_{i=0}^{N-1} (x_iy_i - mx_i^2 - bx_i) = 0$$
Again, we can divide both sides by $-2$:
$$\sum_{i=0}^{N-1} (x_iy_i - mx_i^2 - bx_i) = 0$$
And we can distribute the summation operator and move the constants $m$ and $b$ outside the summations:
$$\sum_{i=0}^{N-1}x_iy_i - m\sum_{i=0}^{N-1}x_i^2 - b\sum_{i=0}^{N-1}x_i = 0$$
Next, we substitute in our earlier result for $b$:
$$\sum_{i=0}^{N-1}x_iy_i - m\sum_{i=0}^{N-1}x_i^2 - \left(\frac{1}{N}\sum_{i=0}^{N-1} y_i - m\left(\frac{1}{N}\sum_{i=0}^{N-1}x_i\right)\right)\left(\sum_{i=0}^{N-1}x_i\right) = 0$$
And we distribute the multiplication:
$$\sum_{i=0}^{N-1}x_iy_i - m\sum_{i=0}^{N-1}x_i^2 - \frac{1}{N}\left(\sum_{i=0}^{N-1} y_i\right)\left(\sum_{i=0}^{N-1}x_i\right) + m\left(\frac{1}{N}\sum_{i=0}^{N-1}x_i\right)\left(\sum_{i=0}^{N-1}x_i\right) = 0$$
Then we can move the terms with $m$ in them to the right-hand side of the equation:
$$\sum_{i=0}^{N-1}x_iy_i - \frac{1}{N}\left(\sum_{i=0}^{N-1} y_i\right)\left(\sum_{i=0}^{N-1}x_i\right) = m\sum_{i=0}^{N-1}x_i^2 - m\left(\frac{1}{N}\sum_{i=0}^{N-1}x_i\right)\left(\sum_{i=0}^{N-1}x_i\right)$$
And, to make the next step a _bit_ neater, we can multiply both sides by $N$:
$$N\sum_{i=0}^{N-1}x_iy_i - \left(\sum_{i=0}^{N-1} y_i\right)\left(\sum_{i=0}^{N-1}x_i\right) = mN\sum_{i=0}^{N-1}x_i^2 - m\left(\sum_{i=0}^{N-1}x_i\right)\left(\sum_{i=0}^{N-1}x_i\right)$$
Dividing through to isolate $m$ gives us the following:
$$\frac{\displaystyle{N\sum_{i=0}^{N-1}(x_iy_i) - \left(\sum_{i=0}^{N-1} y_i\right)\left(\sum_{i=0}^{N-1}x_i\right)}}{\displaystyle{N\sum_{i=0}^{N-1}(x_i^2) - \left(\sum_{i=0}^{N-1}x_i\right)^2}} = m$$
Multiplying by ($\frac{1/N}{1/N}$) gets us to a slightly different form:
$$\frac{\displaystyle{\sum_{i=0}^{N-1}(x_iy_i) - \frac{1}{N}\left(\sum_{i=0}^{N-1} y_i\right)\left(\sum_{i=0}^{N-1}x_i\right)}}{\displaystyle{\sum_{i=0}^{N-1}(x_i^2) - \frac{1}{N}\left(\sum_{i=0}^{N-1}x_i\right)^2}} = m$$
Recalling the definitions of _sample variance_ and _sample covariance_, we can see that this is equivalent to:
$$\frac{\text{cov}(X, Y)}{\text{Var}(X)} = m$$
<subsection label="results">Results</subsection>
The analysis above leads us to the following conclusion:
$$m = \frac{\text{cov}(X, Y)}{\text{Var}(X)}$$
and
$$b = \overline{Y} - m\overline{X}$$
Here, the first equation tells us that the slope of the "best fit" line is the
ratio of the covariance of $X$ and $Y$ to the variance of $X$. The second
tells us that the line must pass through the point $(\overline{X}, \overline{Y})$.
<section>Pearson's R</section>
We will also define the Pearson Correlation Coefficient of two variables $X$ and $Y$ as:
$$r = \frac{\text{cov}(X,Y)}{\sigma_X\sigma_Y}$$
This number $r$, which is between $-1$ and $1$, tells us how strongly, and in what direction, the two variables are correlated: a value of 1 implies total positive linear correlation, 0 implies no linear correlation, and -1 implies total negative linear correlation.
<section>Your Task</section>
Your task in this exercise is to implement several Python functions related to
the discussion above:
* `sample_mean(values)` should take as its lone argument a list of $x$ values, and it should return the sample mean of those values.
* `sample_var(values)` should take a list of $x$ values as its argument, and it should return the sample variance of those values.
* `sample_std(values)` should take a list of $x$ values as its argument, and it should return the sample standard deviation of those values.
* `sample_cov(x_values, y_values)` should take a list of $x$ values and a list of $y$ values as its arguments, and it should return the sample covariance of $X$ and $Y$.
* `pearson_r(x_values, y_values)` should take a list of $x$ values and a list of $y$ values as its arguments, and it should return Pearson's r value for $X$ and $Y$.
* `linear_regression(x_values, y_values)` should take a list of $x$ values and a list of $y$ values as its arguments, and it should return a tuple of three elements, in this order:
* the slope of the "best-fit" line,
* the vertical intercept of the "best-fit" line, and
* the Pearson r value for the regression
**You should implement this code _without the use of `numpy` or `scipy`_.**
You will want to do some testing on your own machine first! Many explanations
of lineare regression have small examples that are good for testing (for
example, . You can also try doing a test with values that are exactly on the
same line (what should the $m$, $b$, and $r$ values be in that case?), or on
other small data sets.
In order to keep things organized, you should try to implement these functions
in terms of the others whenever possible!
<section>Submission</section>
<question pythoncode>
csq_interface="upload"
csq_show_skeleton=False
csq_sandbox_options = {'BADIMPORT': {'numpy', 'scipy'}}
import ast
import random
def check_num(x, y):
try:
return abs(x['result']-y['result']) <= 1e-2
except:
return False
csq_tests = [
{'code': 'values = %r\nans=sample_mean(values)' % [round(random.uniform(-20,10), 2) for i in range(random.randint(8, 25))], 'check_function': check_num} for i in range(3)
]
csq_tests += [
{'code': 'values = %r\nans=sample_var(values)' % [round(random.uniform(-20,10), 2) for i in range(random.randint(8, 25))], 'check_function': check_num} for i in range(3)
]
csq_tests += [
{'code': 'values = %r\nans=sample_std(values)' % [round(random.uniform(-20,10), 2) for i in range(random.randint(8, 25))], 'check_function': check_num} for i in range(3)
]
def checkregression(sub, soln):
x = sub['result']
y = soln['result']
return len(x[-1]) == len(y[-1]) and all(abs(i-j) <= 1e-2 for i,j in zip(x[-1], y[-1]))
def mfunc(x):
return '<tt>%r</tt><br/>%s' % (x[-1], make_regress_plot(x[0], x[1], x[2][0], x[2][1]))
csq_covar_tests = []
csq_pearson_tests = []
csq_linreg_tests = []
for i in range(2):
m = random.uniform(-10,10)
b = random.uniform(-100,100)
x = [round(random.uniform(-30,40), 2) for i in range(random.randint(20,200))]
y = [m*i+b for i in x]
csq_linreg_tests.append({'code': 'xvals = %r\nyvals = %r\nans = [xvals, yvals, linear_regression(xvals, yvals)]' % (x, y)})
if i%2:
csq_covar_tests.append({'code': 'xvals = %r\nyvals = %r\nans = sample_cov(xvals, yvals)' % (x, y)})
csq_pearson_tests.append({'code': 'xvals = %r\nyvals = %r\nans = pearson_r(xvals, yvals)' % (x, y)})
for i in range(5):
x = [round(random.uniform(-50,70), 2) for i in range(random.randint(20,200))]
y = [round(random.uniform(-80,-90), 2) for i in x]
csq_linreg_tests.append({'code': 'xvals = %r\nyvals = %r\nans = [xvals, yvals, linear_regression(xvals, yvals)]' % (x, y)})
if i%2:
csq_covar_tests.append({'code': 'xvals = %r\nyvals = %r\nans = sample_cov(xvals, yvals)' % (x, y)})
csq_pearson_tests.append({'code': 'xvals = %r\nyvals = %r\nans = pearson_r(xvals, yvals)' % (x, y)})
for i in range(5):
m = random.uniform(-10,10)
b = random.uniform(-100,100)
x = [round(random.uniform(-1000,1000), 2) for i in range(random.randint(20,200))]
y = [random.gauss(m*i+b, 100) for i in x]
csq_linreg_tests.append({'code': 'xvals = %r\nyvals = %r\nans = [xvals, yvals, linear_regression(xvals, yvals)]' % (x, y)})
if i%2:
csq_covar_tests.append({'code': 'xvals = %r\nyvals = %r\nans = sample_cov(xvals, yvals)' % (x, y)})
csq_pearson_tests.append({'code': 'xvals = %r\nyvals = %r\nans = pearson_r(xvals, yvals)' % (x, y)})
for i in csq_covar_tests + csq_pearson_tests:
i['check_function'] = check_num
for i in csq_linreg_tests:
i['check_function'] = checkregression
i['transform_output'] = mfunc
csq_tests.extend(csq_covar_tests)
csq_tests.extend(csq_pearson_tests)
csq_tests.extend(csq_linreg_tests)
csq_soln = """def sample_mean(xs):
return sum(xs) / len(xs)
def sample_var(xs):
return sample_std(xs)**2
def sample_std(xs):
m = sample_mean(xs)
return (sum([(i-m)**2 for i in xs]) / (len(xs)-1))**0.5
def sample_cov(xs, ys):
x_mean = sample_mean(xs)
y_mean = sample_mean(ys)
o = 0.0
for index in range(len(xs)):
o += (xs[index] - x_mean)*(ys[index] - y_mean)
return o / (len(xs) - 1)
def pearson_r(xs, ys):
return sample_cov(xs, ys) / (sample_std(xs)*sample_std(ys))
def linear_regression(xs, ys):
r = pearson_r(xs, ys)
m = r * sample_std(ys) / sample_std(xs)
b = sample_mean(ys) - m * sample_mean(xs)
return m, b, r"""
</question>

@ -0,0 +1,146 @@
cs_long_name = 'Linear Regression'
mode='analysis'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as _p
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk, FigureCanvasAgg
from io import BytesIO
import base64 as _b64
class PlotWindowBase(_p.Figure):
"""
Tk window containing a matplotlib plot. In addition to the functions
described below, also supports all functions contained in matplotlib's
Axes_ and Figure_ objects.
.. _Axes: http://matplotlib.sourceforge.net/api/axes_api.html
.. _Figure: http://matplotlib.sourceforge.net/api/figure_api.html
"""
def __init__(self, title="Plotting Window", visible=True):
"""
:param title: The title to be used for the window initially
:param visible: Whether to actually display a Tk window (set to
``False`` to create and save plots without a window
popping up)
"""
_p.Figure.__init__(self)
self.ax = self.add_subplot(111)
self.visible = visible
if self.visible:
self.canvas = FigureCanvasTkAgg(self, tkinter.Tk())
self.title(title)
self.makeWindow()
self.show()
else:
self.canvas = FigureCanvasAgg(self)
def makeWindow(self):
"""
Pack the plot and matplotlib toolbar into the containing Tk window
(called by initializer; you will probably never need to use this).
"""
self.canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=1)
self.toolbar = NavigationToolbar2Tk( self.canvas, self.canvas._master )
self.toolbar.update()
self.canvas._tkcanvas.pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=1)
def destroy(self):
"""
Destroy the Tk window. Note that after calling this method (or
manually closing the Tk window), this :py:class:`PlotWindow` cannot be
used.
"""
try:
self.canvas._master.destroy()
except:
pass # probably already destroyed...
def clear(self):
"""
Clear the plot, keeping the Tk window active
"""
self.clf()
self.add_subplot(111)
if self.visible:
self.show()
def show(self):
"""
Update the canvas image (automatically called for most functions)
"""
self.canvas.draw()
def __getattr__(self, name):
show = True
if name.startswith('_'):
name = name[1:]
show = False
if hasattr(self.axes[0], name):
attr = getattr(self.axes[0], name)
if hasattr(attr,'__call__'):
if show:
def tmp(*args,**kwargs):
out = attr(*args,**kwargs)
if self.visible:
self.show()
return out
return tmp
else:
return attr
else:
return attr
else:
raise AttributeError("PlotWindow object has no attribute %s" % name)
def title(self,title):
"""
Change the title of the Tk window
"""
self.ax.set_title(title)
def legend(self, *args):
"""
Create a legend for the figure (requires plots to have been made with
labels)
"""
handles, labels = self.axes[0].get_legend_handles_labels()
self.axes[0].legend(handles, labels)
if self.visible:
self.show()
def save(self, fname):
"""
Save this plot as an image. File type determined by extension of filename passed in.
See documentation for savefig_.
:param fname: The name of the file to create.
.. _savefig: http://matplotlib.sourceforge.net/api/figure_api.html
"""
self.savefig(fname)
def stay(self):
"""
Start the Tkinter window's main loop (e.g., to keep the plot open at
the end of the execution of a script)
"""
self.canvas._master.mainloop()
def PlotWindow(title="Plotting Window"):
class _PlotWindow(PlotWindowBase):
def __init__(self, title="Plotting Window"):
PlotWindowBase.__init__(self, title, visible=False)
def _show(self, width="75%"):
sio = BytesIO()
self.savefig(sio, format='png', facecolor='none')
return '<img src="data:image/png;base64,%s" width="%s"/>' % (_b64.b64encode(sio.getvalue()).decode(), width)
return _PlotWindow(title)
ys = [-26.85901649618508, -4.82309989277328, 358.12325659886665, -236.27303224345013, 78.96311735375048, -122.0718747926675, -91.794917932851, 156.92288603409375, 77.76372162876706, 134.08018802466293, 421.2164012311623, -102.44084767789477, 69.05193026508634, -65.1707634424306, -6.811873517536455, 14.464555720475445, 123.35092804160584, 269.2411300965563, 283.6126314082743, 249.5593597768601, -45.93699758535121, 160.9929838505804, -11.486651027732734, 34.89429129194224, 227.0114874496006, 453.94991714737176, 159.59340132582852, 305.53172734934645, 209.30183669664237, 377.794764790121, 316.6853125571703, 242.97813270158048, 99.55136064457564, 380.8281222370041, 517.9678491133832, 152.82974416562115, 501.3107247844115, 385.00869117981006, 260.76894288405657, 245.62085551544348, 381.93212971867905, 282.37383569434684, 255.6684633511091, 528.9665009201663, 214.89786230001818, 198.47447067627894, 583.0632870160896, 556.0811185746013, 334.2809525610168, 743.3044708948721, 279.7165711058779, 755.3224521456882, 200.5387369220224, 138.47687852393108, 197.63575541409483, 181.71767331525095, 376.89074479245284, 652.3613138217629, 381.4275586382469, 476.0995327254289, 461.46417474466193, 423.8593541888392, 503.3792036069044, 567.7438934709394, 296.9627154414269, 183.36747560528335, 714.4663522847009, 620.296512084993, 315.16553989225656, 644.953867681399, 381.11694172321086, 392.7101550383068, 474.0136908544534, 556.91031778183, 625.6146302263054, 561.5239749404492, 491.3389262641957, 396.09862792366937, 328.9611909946891, 442.51020674282574, 637.3002734137215, 820.1180066579375, 704.0222105099904, 749.7416740127572, 571.3375044210943, 740.3732575510189, 508.23757383850227, 805.9057544835931, 652.1781007402574, 331.2078936574819, 659.394649695742, 779.5249873375656, 417.1728951942524, 727.5381994766699, 846.8237700909708, 372.6078315611338, 735.4394177096567, 359.3157568184622, 742.5223675039202, 645.445434892132]
xs = [103+i for i in range(len(ys))]

@ -0,0 +1 @@
cs_long_name = 'Questions'

@ -0,0 +1,98 @@
<section>Page Specification and Loading</section>
CAT-SOOP courses are stored using files on disk. The structure of the web site
mirrors the structure of the directories on disk. Having a directory at
`course_root/page` means that a page will be available at
`http://my.site/page`.
Names beginning with underscores or dots are hidden (not web accessible), so if
you plan on storing other things inside the same repository, you should make
sure their names start with an underscore.
Inside of each page's directory, there should be a file called
`content.catsoop`. This file contains the page's source (see the
[Markdown](COURSE/markdown) and [Questions](COURSE/questions) pages for more
information about how these files are structured).
<subsection>Magic Variables and Inheritance</subsection>
A lot of behavior in CAT-SOOP is defined by the values stored in "magic"
variables with particular names. These variables control various aspects of
the page's appearance, among other things. Typically, all magic variables
begin with the prefix `cs_` (or `csq_` if they control aspects of a _question_
rather than a page).
In addition to `content.catsoop`, each directory can also contain a file called
`preload.py`, which is typically used to set these magic variables. Each page
inherits the values resulting from the `preload.py` "above" it in the directory
structure. In particular, on each page load:
* An empty environment $E_1$ is created
* All the `preload.py` files leading up to the requested page are executed in
$E_1$, one after the other, starting with the root.
* User authentication occurs
* Includes are processed
* All Python tags on the given page are executed in $E_1$
* Each question tag on the page creates a new environment $E_i$ inheriting from
$E_1$, and the code from the question tag is evaluated in $E_i$
So values set in the top-level `preload.py` affect all pages beneath them
(unless those values are overridden). Values set in Python tags in a
particular page's `content.catsoop` affect only that page, and values set in
`question` tags on a page affect only that particular question.
<section>Static Files and Linking</section>
Static files (i.e., those not generated by a `content.catsoop`) are treated
differently. They should be placed inside a directory called `__STATIC__`
within a page's folder.
Both static files and dynamic content can be linked to using a short-hand
syntax. For example, imagine that there is a page `course_root/homeworks/week1`
and a static file `course_root/homeworks/__STATIC__/an_image.png`.
In the `content.catsoop` for the course, these could be linked to as `CURRENT/week1`
and `CURRENT/an_image.png`, respectively. `CURRENT` always refers to the
location of the page currently being loaded.
Similarly, from any page within the course, those files could be linked to as
`COURSE/homeworks/week1` and `COURSE/homeworks/an_image.png`, respectively.
<section>Controlling Access to Pages</section>
<subsection>Permissions</subsection>
Users' roles are determined by the files in the `__USERS__` directory. Each
user should have a file there, named like `username.py`. One way to handle
permissions is to define a variable called `permission` in each of these files,
which points to a list containing one or more strings describing what that user
is allowed to do:
* `'view'`: allowed to view the contents of a page
* `'submit'`: allowed to submit to a page
* `'view_all'`: always allowed to view every page, regardless of when it releases
* `'submit_all'`: always allowed to submit to every question, regardless of when it releases or is due
* `'impersonate'`: allowed to view the page "as" someone else
* `'admin'`: administrative tasks (such as modifying group assignments)
* `'whdw'`: allowed to see "WHDW" page (Who Has Done What)
* `'email'`: allowed to send e-mail through CAT-SOOP
* `'grade'`: allowed to submit grades
Since many groups of users often share the same permission levels, a more
common way to handle permissions is to assign each user a 'role', and to define
`cs_permissions` in `preload.py` as a dictionary mapping role names to the
permissions associated with that role. Take a look at `preload.py`, where such
a mapping is defined. A couple of `__USERS__` files are also included in this
sample course.
<subsection>Release and Due Dates</subsection>
`cs_release_date` controls the date at which a page is "released" to a student
(i.e., that it becomes visible to those with the `view` permission). It can
always be specified as a string `"YYYY-MM-DD:HH:MM"`, and it can also be
specified based on course-specific meeting times (see the `preload.py` file for
more information).
Similarly, `cs_due_date` controls when the questions on a page come due. In
CAT-SOOP, the default behavior is to allow submissions after the due date. To
implement a hard deadline and lock students out from submitting after a
deadline, set `cs_auto_lock = True`.

@ -0,0 +1 @@
cs_long_name = 'Course Structure'
Loading…
Cancel
Save