def comprehension(origin=None): lb, ub = fcast_app.correct_bins[0], fcast_app.correct_bins[-1] init_bins = list((np.array(fcast_app.correct_bins)-lb)/(ub-lb)) init_prob = [1./(len(init_bins)-1)] * (len(init_bins)-1) return Branch( *comprehension_check( instructions=Page( Label( ''' <p>Watch the full instructional video before continuing. </p> ''' + instructions_vid ), timer=('InstructionsTime', -1) ), checks=Page( gen_dashboard( src='/fcast-instr/', bins=init_bins, prob=init_prob, var='CompCheck', data_rows=-1, submit=S.verify_fcast() ), back=True ), attempts=3 ), navigate=N.fcast() )
def fcast(origin=None): questions = texts.fcast_questions.copy() n_bins_list = [choice(N_BINS) for q in questions] fcast_pages = [ Page( gen_dashboard( '/fcast/', label=label, n_bins=n_bins, var='Forecast', record_order=True ), timer='ForecastTime', embedded=[Embedded('Variable', var), Embedded('NBins', n_bins)] ) for (var, label), n_bins in zip(questions, n_bins_list) ] shuffle(fcast_pages) for i, page in enumerate(fcast_pages): page.questions.insert( 0, Label( '<p><b>Question {} of {}</b></p>'.format(i+1, len(fcast_pages)) ) ) return Branch( *fcast_pages, Page( Label(texts.completion), terminal=True ) )
def seed(): """Creates the main survey branch. Returns: List[Page]: List of pages shown to the user. """ return [ Page( Label("Hello, world!") ), Page( Label("Goodbye, world!"), back=True ) ]
def demographics( *items, page=False, require=False, record_index=False, ): """ Parameters ---------- \*items : str Names of demographic items to return. [See the full list of available items](items.md). page : bool, default=False Indicates that a page should be returned containing the demographics items. If `False`, a list of questions is returned. require : bool, default=False Indicates that participants are required to respond to the items. record_index : bool, default=False Indicates that the dataframe should record the order in which the demographic items appear on the page. Returns ------- demographics : hemlock.Page or list of hemlock.Question A page containing the requested demographics items if `page`, otherwise a list of demographics questions. Examples -------- ```python from hemlock import push_app_context from hemlock_demographics import demographics app = push_app_context() demographics('age', 'gender', 'race', page=True).preview() ``` """ def add_question(item): q = demographics_items[item](require) (questions.extend(q) if isinstance(q, (tuple, list)) else questions.append(q)) def set_question_attrs(question): question.data_rows = -1 question.record_index = record_index questions = [] [add_question(item) for item in items] [set_question_attrs(q) for q in questions] if page: return Page(*questions, name='Demographics', timer=('DemographicsTime', -1), debug=[D.debug_questions(), D.forward()]) return questions
def make_second_estimate_page(i, key, questions): """ :param i: estimate number :type i: int :param key: name of time-series :type key: str :param questions: corresponding first estimate questions :type questions: list of hemlock.Blank :return: page asking for second estimates :rtype: hemlock.Page """ labels = make_fcast_question_labels(key, context) return Page( Label(progress(i / N_FCASTS, f'Estimate {i+1} of {N_FCASTS}')), Dashboard(src='/dashapp/', g={ 'fcast_key': key, 'context': context }), Label(f''' Your first estimates were: {make_list( [ label[0]+q.response+label[1] for label, q in zip(labels, questions) ] )} '''), # additional questions (i.e., for dialectical bootstrapping) *make_additional_questions(), # estimation questions *make_fcast_questions(key, context, first_estimate=False), timer='SecondEstimateTime')
def ultimatum_game(start_branch): def gen_check_page(accept): return Page(Label(), Input('How much money does the proposer receive?', prepend='$', append='.00', type='number', required=True), Input('How much money does the responder receive?', prepend='$', append='.00', type='number', required=True), compile=[C.clear_response(), C.random_proposal(accept)]) proposer = assigner.next()['Proposer'] return Branch( *comprehension_check( instructions=Page( Label( open('ug_instructions.md', 'r').read()\ .format(POT=POT, N_ROUNDS=N_ROUNDS) ) ), checks=[ gen_check_page(accept=True), Page( Label('Correct! One more check to pass.') ), gen_check_page(accept=False) ], attempts=3 ), Page( Label('You passed the comprehension check!') ), Page( Label( f"You are about to play an ultimatum game as a **{'proposer' if proposer else 'responder'}**." ) ), navigate=proposer_branch if proposer else responder_branch )
def start(): return Branch( *comprehension_check( instructions=Page( Label('Here are some instructions') ), checks=Page( Check( '<p>Select the correct choice.</p>', ['Correct', 'Incorrect', 'Also incorrect'], compile=[C.clear_response(), C.shuffle()], validate=V.require(), submit=S.correct_choices('Correct') ) ), attempts=3 ), Page( Label('You passed the comprehension check!'), terminal=True ) )
def _verify1(q1, require): if q1.data != CORRECT_1: page = Page(Input(''' Imagine we are throwing a five-sided die 50 times. On average, out of these 50 throws how many times would this five-sided die show an odd number (1, 3, or 5)? ''', append='out of 50 throws', type='number', min=0, max=50, step=1, required=require, var='Berlin2a', data_rows=-1, submit=_verify2a, debug=[D.send_keys(), D.send_keys('30', p_exec=.5)]), name='Berlin 2a', timer=('Berlin2aTime', -1), debug=[D.debug_questions(), D.forward()]) else: page = Page(Input(''' Imagine we are throwing a loaded die (6 sides). The probability that the die shows a 6 is twice as high as the probability of each of the other numbers. On average, out of these 70 throws how many times would the die show the number 6? ''', append='out of 70 throws', type='number', min=0, max=70, required=require, var='Berlin2b', data_rows=-1, submit=S(_verify2b, require), debug=[D.send_keys(), D.send_keys('20', p_exec=.2)]), name='Berlin 2b (or not 2b?)', timer=('Berlin2bTime', -1), debug=[D.debug_questions(), D.forward()]) q1.branch.pages.insert(q1.page.index + 1, page)
def gen_rate_articles_page(name, headline, url): return Page( RangeInput(''' <p>Take a few minutes to read <a href="{}" target="_blank">this article</a>.</p> {} <p>From 0 (not at all ) to 10 (very much), how much did you enjoy reading that article?</p> '''.format(url, headline), min=0, max=10, required=True, var='Enjoy'))
def first_estimates_branch(start_branch=None): """ :param start_branch: :type start_branch: hemlock.Branch :return: branch with first estimate questions :rtype: hemlock.Branch """ def make_first_estimate_questions(): """ :return: first estimate questions :rtype: list of hemlock.Blank """ fcast_keys = fcast_selector.next() fcast_keys = (list(fcast_keys) if isinstance(fcast_keys, tuple) else [fcast_keys]) shuffle(fcast_keys) current_user.embedded.append(Embedded('Forecast', fcast_keys)) return [(key, make_fcast_questions(key, context, first_estimate=True)) for key in fcast_keys] assigner.next() context = use_context(first_estimate=True) first_estimate_questions = make_first_estimate_questions() return Branch( Page(Label(INSTRUCTIONS['first'][current_user.meta['Context']])), *[ Page(Label(progress(i / N_FCASTS, f'Estimate {i+1} of {N_FCASTS}')), Dashboard(src='/dashapp/', g={ 'fcast_key': key, 'context': context }), *questions, timer='FirstEstimateTime') for i, (key, questions) in enumerate(first_estimate_questions) ], navigate=N.second_estimates_branch(first_estimate_questions))
def gen_check_page(accept): return Page(Label(), Input('How much money does the proposer receive?', prepend='$', append='.00', type='number', required=True), Input('How much money does the responder receive?', prepend='$', append='.00', type='number', required=True), compile=[C.clear_response(), C.random_proposal(accept)])
def start(): """ :return: branch with consent form and preliminary questions :rtype: hemlock.Branch """ return Branch(consent_page(open('texts/consent.md', 'r').read()), Page(attention_check()), basic_demographics(page=True), *crt('bat_ball', 'flowers', 'students', 'green_round', 'stock', 'whales', page=True), berlin(), navigate=first_estimates_branch)
def crt(*items, page=False, require=False, shuffle=False): """ Parameters ---------- \*items : str The names of CRT items. If no items are given, the standard 3-item CRT is used. [See the full list of available items.](items.md). page : bool, default=False Indicates that items should be in separate pages. require : bool, default=False Indicates that responses are required. shuffle : bool, default=False Indicates that items should be shuffled. Returns ------- CRT items : list List of `hemlock.Question` if not `page`, otherwise list of `hemlock.Page`. """ if 'CRT_Total' not in current_user.g: current_user.g.update({ 'CRT_Total': 0, 'CRT_Correct': 0, 'CRT_Intuitive': 0 }) items = items if items else ['bat_ball', 'widgets', 'lily_pads'] items = [crt_items[item]() for item in items] if require: for item in items: item.validate.append(V.require()) if page: items = [ Page(item, name=item.var, timer=(item.var + 'Time', -1), debug=[D.debug_questions(), D.forward()]) for item in items ] if shuffle: random.shuffle(items) return items
def start(): return Branch( Page( Label(texts.consent_label), Check( choices=[('I consent to participate', 'consent')], validate=V.correct_choices( 'consent', error_msg='<p>Please consent to participate.</p>' ) ) ), demographics( 'age_bins', 'gender', 'race', 'education', page=True, require=True ), *crt(page=True, require=True), berlin(require=True), navigate=N.comprehension() )
def proposer_branch(ug_branch): branch = Branch(navigate=end) for i in range(N_ROUNDS): branch.pages += [ Page( Label(progress(i / N_ROUNDS, f'Round {i+1} of {N_ROUNDS}')), proposal_input := Blank((f''' You have ${POT} to split between you and the responder. Fill in the blank: I would like to offer the responder **$''', '.00**.'), prepend='$', append='.00', var='Proposal', blank_empty='__', type='number', min=0, max=POT, required=True)), Page(Label(compile=C.display_proposer_outcome(proposal_input))) ] return branch
def responder_branch(ug_branch): branch = Branch(navigate=end) for i in range(N_ROUNDS): branch.pages += [ Page( Label(progress(i / N_ROUNDS, f'Round {i+1} of {N_ROUNDS}')), response_input := Blank((f''' The proposer has ${POT} to split between him/herself and you. Fill in the blank: I will accept any proposal which gives me at least **$''', '.00**.'), prepend='$', append='.00', var='Response', blank_empty='__', type='number', min=0, max=POT, required=True)), Page(Label(compile=C.display_responder_outcome(response_input))) ] return branch
def berlin(require=False): """ Add the Berlin Numeracy Test to a hemlock survey. Parameters ---------- require : bool, default=False Indicates that responses are required. Returns ------- Berlin page 1 : hemlock.Page The first page of the Berlin Numeracy Test. Notes ----- Although this function returns only the first page of the test, it is all you need to add the full test to your survey. The submit function of the page returned by this function adaptively generates additional pages of the test. """ return Page(Input(''' Out of 1,000 people in a small town 500 are members of a choir. Out of these 500 members in the choir 100 are men. Out of the 500 inhabitants that are not in the choir 300 are men. What is the probability that a randomly drawn man is a member of the choir? Please enter the probability as a percent. ''', append='%', type='number', min=0, max=100, step='any', required=require, var='Berlin1', data_rows=-1, submit=S(_verify1, require), debug=[D.send_keys(), D.send_keys('25', p_exec=.5)]), name='Berlin 1', timer=('Berlin1Time', -1), debug=[D.debug_questions(), D.forward()])
def _verify2b(q2b, require): if q2b.data == CORRECT_2B: _record_score(q2b, score=4) else: page = Page(Input(''' In a forest 20% of mushrooms are red, 50% brown, and 30% white. A red mushroom is poisonous with a probability of 20%. A mushroom that is not red is poisonous with a probability of 5%. What is the probability that a poisonous mushroom in the forest is red? ''', append='%', type='number', min=0, max=100, step='any', required=require, var='Berlin3', data_rows=-1, submit=_verify3, debug=[D.send_keys(), D.send_keys('50', p_exec=.5)]), name='Berlin 3', timer=('Berlin3Time', -1), debug=[D.debug_questions(), D.forward()]) q2b.branch.pages.insert(q2b.page.index + 1, page)
def start(): return Branch( *crt('bat_ball', 'lily_pads', 'widgets', 'students', page=True), Page(Label(compile=display_score), terminal=True))
def start(): conditions = assigner.next() return Branch(Page(Label('Page ' + conditions['set0'])), Page(Label('Page ' + conditions['set1'])), Page(Label('The end!'), terminal=True))
def big5(*items, version='IPIP-50', page=False, choices=5, include_instructions=True, shuffle_items=False, record_index=False): """ Create a big 5 personality questionnaire. Parameters ---------- \*items : Names of big 5 items to include. If no items are specified, this function returns all big 5 items in the given version. version : str, default='IPIP-50' Version of the big 5 questionnaire. Currently supported are `'IPIP-50'` (50-item version from the Interntaional Personality Item Pool), `'TIPI'` (Ten-Item Personality Inventory), and `'BFI-10'` (10-item Big 5 Inventory). page : bool, default=False Indicates that this function should return a page with the big 5 items. Otherwise, return a list of questions. choices : int or list Passed to `hemlock.likert`. 5, 7, and 9 mean 5-, 7-, and 9-point Likert scales. Alternatively, pass a list of strings. include_instructions : bool, default=True Indicates that an instructions label should be included before the items. shuffle_items : bool, default=False Indicates that items should be shuffled. record_index : bool, default=False Indicates to record the index of the big 5 items as they appear on the page. Only applies of `page` is `True`. Returns ------- big5_questionnaire : hemlock.Page or list of hemlock.Question If `page` is `True`, this function returns a page containing the requested big 5 items. Otherise, it returns a list of questions. """ def gen_question(item): # generates a question for a given big 5 item _, label, ascending = item_bank[item] return likert( label, choices=choices, reversed=not ascending, var='Big5' + item, data_rows=-1, record_index=record_index, ) item_bank = _get_item_bank(version) if not items: items = item_bank.keys() questions = [gen_question(item) for item in items] if shuffle_items: shuffle(questions) if include_instructions: questions.insert(0, Label(instructions_label)) if page: return Page(*questions, name='Big5', timer=('Big5Time', -1), submit=partial(_record_score, item_bank)) return questions
def end(rounds_branch): return Branch( Page(Label('Thank you for completing the survey!'), terminal=True))
def start(): return Branch(demographics_page := basic_demographics(page=True), Page( Label(compile=C.confirm_demographics(demographics_page)), back=True), navigate=ultimatum_game)
def start(): return Branch(comprehensive_demographics(page=True), Page(Label('The end.'), terminal=True))
def start(): return Branch(berlin(), Page(Label(compile=display_score), terminal=True))
def second_estimates_branch(first_estimate_branch, first_estimate_questions): """Create branch for second estimates :param first_estimate_branch: branch for first estimates :type first_estimate_branch: hemlock.Branch :param first_estimate_questions: questions containing participant's first estimates :type first_estimate_questions: list of (time-series key, question) tuples :return: second estimates branch :rtype: hemlock.Branch """ def make_instructions_labels(): """ :return: instructions labels for second estimates :rtype: list of hemlock.Label """ labels = [Label(open('texts/second_estimate.md', 'r').read())] if current_user.meta['Context'] == 'second-only': labels.append(Label(open('texts/second_estimate_addcontext.md'))) return labels def make_second_estimate_page(i, key, questions): """ :param i: estimate number :type i: int :param key: name of time-series :type key: str :param questions: corresponding first estimate questions :type questions: list of hemlock.Blank :return: page asking for second estimates :rtype: hemlock.Page """ labels = make_fcast_question_labels(key, context) return Page( Label(progress(i / N_FCASTS, f'Estimate {i+1} of {N_FCASTS}')), Dashboard(src='/dashapp/', g={ 'fcast_key': key, 'context': context }), Label(f''' Your first estimates were: {make_list( [ label[0]+q.response+label[1] for label, q in zip(labels, questions) ] )} '''), # additional questions (i.e., for dialectical bootstrapping) *make_additional_questions(), # estimation questions *make_fcast_questions(key, context, first_estimate=False), timer='SecondEstimateTime') def make_additional_questions(): """Make additional questions, such as prompts for dialectical bootstrapping :return: additional questions :rtype: list of hemlock.Question """ if not current_user.meta['Bootstrap']: return [ Label(''' Please make second estimates which are different from your first estimates. ''') ] return [ Textarea(''' Imagine your first estimates were off the mark. Write at least one reason why that could be. Which assumptions or considerations could have been wrong? ''', var='Assumptions', required=True, validate=V.min_words(7), debug=[ D.send_keys('here are 7 words without a meaning'), D.send_keys(p_exec=.2) ]), Check(''' What does this reason imply? Were your first estimates too high or too low? ''', [('Too high', 'high'), ('Too low', 'low'), ('Some were too high, others too low', 'both')], var='Direction', validate=V.require()), Label(''' Based on this new perspective, make second estimates which are different from your first estimates. ''') ] context = use_context(first_estimate=False) return Branch( Page(Label(INSTRUCTIONS['second'][current_user.meta['Context']])), *[ make_second_estimate_page(i, key, questions) for i, (key, questions) in enumerate(first_estimate_questions) ], Page(*[ likert( f'Compared to the average person, how much do you know about {forecast_questions[key]["know_about"]}?', [ 'Much less than average', 'Less than average', 'About average', 'More than average', 'Much more than average' ], var='ContextKnowledge') for key, _ in first_estimate_questions ]), Page( binary(''' Did you look up the answers to any of the questions we asked? It's important that you answer honestly for research purposes. Your answer won't affect your bonus. ''', var='LookUp', data_rows=-1, validate=V.require()), Textarea( 'Do you have any suggestions for how to improve our study? Feedback is greatly appreciated!', var='AdditionalComments', data_rows=-1)), completion_page())