version 10 #delimit ; clear; set mem 60m; set more off; set obs 1; /* Software by Jennifer Murdock, Economics, U of T, jennifer.murdock@utoronto.ca */ /* All rights reserved. Date of last revision: April 11, 2014 */ /****************************************************************************************** * BASIC INFORMATION ABOUT THIS STATA DO-FILE: * * * * This do-file allows you to flexibly grade raw multiple-choice bubble-form data. * * - It can be run on a computer with Stata version 10 or higher * * - It can be run by a Teaching Assistant (TA) in the graduate computer lab * * - You do NOT need Stata to use the output from this do-file: files and reports * * can be opened with Microsoft Excel, Word, or any text editor * * * * New features available as of October 2012: * * - Addresses the new 10 digit U of T Student Numbers * * - Works with U of T's new scanning software * * - Allows a non-zero minimum point value for each question if you wish * * - Allows simultaneous processing of multiple versions of the test that differ * * in the answer ordering, the question ordering, or even differ in the * * questions themselves so long as all versions have the same number of * * questions and total possible points (otherwise must be processed separately) * * - Detailed marks output includes points earned on each question so that you * * can assess a student's marks on different parts of a test if you wish * * * * Continuing features: * * - Easy to use no matter how you currently keep your student information * * - Allows you to accept more than one answer as correct should the need arise * * - Allows you to adjust students' marks (what our students call "curving") * * - Provides you with a detailed report of student performance for each question * * - Produces a file that you can post that lets students see what answers they * * wrote for each question and whether or not it is correct: no need to return * * the original bubble forms to students (stops ex post cheating). * * - Gives a histogram of students' marks that you can cut-and-paste or post * * - Supports 4 different versions of the test (i.e. question order scrambled) * * - Supports tests where point values vary across questions * * - Uses only final digits of the student # for the marked answers to be posted * * - Supports up to 140 questions (formerly limited to 50) * * - Added flexibility if you choose to accept more than on answer as correct * * - (However, students may only choose one of the five answers A - E or leave * * it blank.) * * - Allows analysis by section (lecture section, tutorial section, or other) * * * * Inputs: * * - A file containing scanned answers (machine produced) (comma delimited: .csv) * * - A file containing the list of registered students (*.csv) * * - Your entries below in this do-file * * * * Outputs: * * - A file with students' marked answers and last few digits of their student * * id # that can be posted. Capital letters for correct answers and lower case * * letters for incorrect answers. (mcq_marks_web_site_*.csv) A formatted * * template for Excel that you may cut-and-paste into is available at: * * http://homes.chass.utoronto.ca/~murdockj/teaching/mcq_marks_web_site.xlsx * * Please use that template for posting your results for students: you may * * convert it to a pdf file for maximum accessibility for students. * * - A file with the detailed students' marks for your records (mcq_marks_*.csv) * * - A file with a detailed report on students' performance (REPORT_*.txt) * * - Two picture files with histograms of students' marks (HISTOGRAM_*.emf and * * HISTOGRAM_alt_*.emf): mathematically identical, choose one that looks better * * * * How to use: * * (1) Place this do-file and all required (input) files in a SINGLE directory (folder) * * (2) Read the instructions below that explain what you must enter (e.g. correct answers)* * in this do-file. Lines where you are expected to enter something marked by the * * symbol /*###*/. Complete these actions according to the instructions. * * > Do NOT change anything except where asked (i.e. at lines starting with /*###*/) * * > Read and FOLLOW INSTRUCTIONS VERY PRECISELY. * * (3) Double click on this do-file to run it * * (4) Carefully review the grade report (REPORT_*.txt) to ensure that you did what you * * did what you meant to do. (This code checks your inputs and alerts you to errors * * or possible errors that you will need to fix or double-check.) * *****************************************************************************************/ /*** START OF USER INSTRUCTIONS AND USER INPUT *******************************************/ /*----------------------------------------------------------------------------------------* * Below, type name of the .csv file with the raw data (scanned answers). It MUST: * * (1) have NO header line (i.e. no variables names): 1st row should be 1st student * * * * (2) have the following information IN THE FOLLOWING ORDER: * * (a) Student's LAST NAME (field on bubble form): string format * * (b) Student's INITIAL (field on bubble form): string format * * (c) Student's FORM (field on bubble form): string format * * (d) Student's STUDENT NUMBER (field on bubble form): integer (numeric) * * (e) Student's letter response to Question 1: string format * * (f) Student's letter response to Question 2: string format * * ... * * ( ) Student's letter response to last question: string format * * (3) be saved as a .csv file. This can be created by opening the file in Excel, * * and using "Save as type: " CSV (Comma delimited)(*.csv) Click OK and yes * * until Excel lets you close the file (don't worry about the messages). * * * * Sample valid entry: local scan QZ3_raw_mcq.csv; * *---------------------------------------------------------------------------------------*/ /*###*/ local scan ; /*----------------------------------------------------------------------------------------* * Below, type name of the .csv file with the list of registered students. It MUST: * * (1) have NO header line (i.e. no variables names): 1st row should be 1st student * * * * (2) have the following information fields IN THE FOLLOWING ORDER: * * (a) Student's first name: string format * * (b) Student's last name: string format * * (c) Student's student id number: integer (numeric format) * * (d) OPTIONAL Student's section: string format (tutorial, lecture, other) * * (e ...) OPTIONAL Any other fields you want in the output marks file * * (3) be saved as a .csv file * * * * Sample valid entry: local rosi eco220_rosi_oct09.csv; * *---------------------------------------------------------------------------------------*/ /*###*/ local rosi ; /*----------------------------------------------------------------------------------------* * Below, type student id numbers of any students who do not want their marks * * posted on course web site (i.e. exclude from mcq_marks_web_site_*.csv). * * Separate numbers with spaces. One blank if no students requested extra privacy. * * * * Sample valid entries: * * local priv 990991448 991643045; * * local priv ; * *---------------------------------------------------------------------------------------*/ /*###*/ local priv ; /*----------------------------------------------------------------------------------------* * Below, type a name identifier of this assessment that will be used to name the * * output files (fills in *) and some variables. Keep it short and descriptive. * * * * Sample valid entries: * * local suff QZ1; * * local suff Test2; * *---------------------------------------------------------------------------------------*/ /*###*/ local suff ; /*----------------------------------------------------------------------------------------* * Below, type student id numbers of any students who are excused from the test. * * Separate numbers with spaces. Type one blank if no students are excused. * * * * Sample valid entries: * * local excd 993863970 993925145 981449894; * * local excd ; * *---------------------------------------------------------------------------------------*/ /*###*/ local excd ; /*----------------------------------------------------------------------------------------* * Below, type total number of multiple choice questions in the test * * * * Sample valid entry: local numq 25; * *---------------------------------------------------------------------------------------*/ /*###*/ local numq ; /*----------------------------------------------------------------------------------------* * Below, type how many points a correct answer is worth if all questions are * * equally weighted. If the point value depends on the question, put one blank here * * and you will be asked to specify the point value of each question below. * * * * Sample valid entries: * * local pts 5; * * local pts ; * *---------------------------------------------------------------------------------------*/ /*###*/ local pts ; /*----------------------------------------------------------------------------------------* * Below, type the minimum points ANY answer is worth, which is the same across all * * questions. The norm is 0 points for ANY answer and positive points for each * * correct answer. However, you can choose to give partial credit for any attempt * * or at the other extreme use negative marking (points off unless your answer is * * correct). * * * * Sample valid entries: * * local min_pts ; /* Assumes you wish for the norm of 0 */ * * local min_pts 0; * * local min_pts 1; * * local min_pts -1; * *---------------------------------------------------------------------------------------*/ /*###*/ local min_pts ; /*----------------------------------------------------------------------------------------* * Below, for each version of the test (unique FORM letter), type correct letter * * answers (can be capital or lower case) associated with that FORM. If you used * * less than 4 different versions enter one blank for the answers for the form * * letters that you did not use. * * * * Up to 4 different versions of the test with FORM CODES A - D are allowed * * * * If you have only one version of your test, then use Form A. It is likely that * * if you have only one version some students did not bother fill out the FORM * * field on their bubble form. Don't worry they will be graded according to * * Form A answers. * * * * ANY STUDENTS THAT LEFT THE FORM FIELD ON THE BUBBLE FORM BLANK ARE ASSUMED TO * * HAVE THE FIRST FORM LETTER YOU SPECIFY BELOW. * * * * For each question enter the correct letter answer followed by a blank. If you * * wish to accept more than one answer as correct then enter each letter that you * * wish to accept with no spaces between them. (e.g. ABCDE to accept any answer) * * * * CHECK YOUR ENTRIES FOR ACCURACY: THESE WILL BE USED TO GRADE STUDENTS' TESTS * * * * Sample valid entries: * * local numf 1; * * local numf 4; * * local anA C C C A B A A B D D C D A D C B C A; * * local anA C C ABCDE A B A A B D D C D A D C B C A; * * local anB C A A B C C C D A A B D D B C A D C; * * local anB ; * * local ptA ; * * local ptA 2 2 2 2 2 2 2 2 5 5 5 5 5 5 5 5 5 5; * *----------------------------------------------------------------------------------------*/ /* Total number of different forms used */ /*###*/ local numf ; /* Correct answers for FORM A */ /*###*/ local anA ; /* Points for correct answer by question for FORM A (not necessary if all worth same points, leave blank) */ /*###*/ local ptA ; /* Correct answers for FORM B */ /*###*/ local anB ; /* Points for correct answer by question for FORM B (not necessary if all worth same points, leave blank) */ /*###*/ local ptB ; /* Correct answers for FORM C */ /*###*/ local anC ; /* Points for correct answer by question for FORM C (not necessary if all worth same points, leave blank) */ /*###*/ local ptC ; /* Correct answers for FORM D */ /*###*/ local anD ; /* Points for correct answer by question for FORM D (not necessary if all worth same points, leave blank) */ /*###*/ local ptD ; /*----------------------------------------------------------------------------------------* * Make entries in this section IF: * * (1) You used more than one form * * (2) The forms differ only in the ordering of the questions (hence the * * letter answer is the same and only the question number differs) * * (3) You would like a report on a question-by-question basis (combining the * * different forms combine all observations for each question) * * OTHERWISE: just skip this section. * * * * Enter the "base" form code below: a letter from A - D. * * For the base form the question order is simply 1 2 3 4 ... up to the number of * * questions. For each of the other forms, enter the question numbers that * * correspond to each question in the base form. For example if Question #1 in the * * base form is Question #7 in another form, then enter 7 first for that other form.* * Put a space, then follow the 7 with the question number that corresponds to * * Question #2 in the base form, and so on. Enter question ordering separated by * * spaces in qo* for each form. For an unused form code enter one blank. * * * * Sample valid entries: * * local base ; * * local base A; * * local qoA ; * * local qoB ; * * local qoA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; * * local qoB 2 20 3 7 8 9 4 5 6 10 11 12 16 17 18 13 14 15 19 1; * * local qoC ; * *---------------------------------------------------------------------------------------*/ /* Select the base form code (report for combined results will refer to question orderings on this form) */ /*###*/ local base ; /* Question numbers that correspond to base form for Form A */ /*###*/ local qoA ; /* Question numbers that correspond to base form for Form B */ /*###*/ local qoB ; /* Question numbers that correspond to base form for Form C */ /*###*/ local qoC ; /* Question numbers that correspond to base form for Form D */ /*###*/ local qoD ; /*----------------------------------------------------------------------------------------* * Indicate any CURVE you want to apply to the marks. * * * * Enter 0's if no curve is desired. * * * * Flat curve: Add a fixed number of percentage points to each student's mark * * Ex: 62% + 5% = 67% * * * * Proportional curve: Add a percent increase to each student's mark * * Ex: 62% * (100 + 5)/100 = 65.1% * * * * Both flat and proportional: Ex: (62% + 5%) * (100 + 5)/100 = 70.35% * * * * Sample valid entries: * * local flat 0; * * local prop 0; * * local flat 8; * * local prop 5; * *---------------------------------------------------------------------------------------*/ /*###*/ local flat 0; /*###*/ local prop 0; /*** END OF USER INSTRUCTIONS AND USER INPUT *********************************************/ /****************************************************************************************** * DO NOT CHANGE ANYTHING BELOW THESE LINES * ******************************************************************************************/ quietly {; tempfile scanned holder sum; /*** Clean-up user inputs ***/ local error = 0; if ("`numq'" == "") {; noisily display "ERROR: Number of questions numq left blank: must be > 0 and <= 140"; noisily display " Fix Stata do-file and re-run."; exit; }; /* end if */ if (`numq' <= 0 | `numq' > 140 | "`numq'" == "") {; noisily display "ERROR: Number of questions numq (=`numq') must be > 0 and <= 140"; noisily display " Fix Stata do-file and re-run."; exit; }; /* end if */ /* Make sure that entered answers are all upper-case */ foreach Z in A B C D {; local an`Z' : subinstr local an`Z' "a" "A"; local an`Z' : subinstr local an`Z' "b" "B"; local an`Z' : subinstr local an`Z' "c" "C"; local an`Z' : subinstr local an`Z' "d" "D"; local an`Z' : subinstr local an`Z' "e" "E"; }; foreach Z in A B C D {; local W : word count `an`Z''; if (`W'~=`numq' & `W'~=0) {; noisily display "ERROR: # of answers you entered for Form `Z' (=`W') not equal to # of questions (=`numq')"; noisily display " Fix your inputs in Stata do-file and re-run."; local error = 1; }; /* end if */ }; /* end for */ if `error' == 1 {; exit; }; local i = 0; foreach Z in A B C D {; if "`an`Z''"~="" {; local i = `i' + 1; }; }; /* end for */ if "`numf'"=="" {; noisily display "ERROR: Please enter the number of forms you used. Check you inputs in Stata do-file."; local error = 1; }; /* end if */ if `error' == 1 {; exit; }; if `i' ~= `numf' {; noisily display "ERROR: Number of forms num_forms (=`numf') is inconsistent with number of forms"; noisily display " for which you entered answers (=`i'). Check you inputs in Stata do-file."; local error = 1; }; /* end if */ if `error' == 1 {; exit; }; foreach Z in A B C D {; if "`an`Z''"~="" {; forvalues Y = 1(1)`numq' {; local W : word `Y' of `an`Z''; local X = "`W'"; local X = subinstr("`X'","A","",1); local X = subinstr("`X'","B","",1); local X = subinstr("`X'","C","",1); local X = subinstr("`X'","D","",1); local X = subinstr("`X'","E","",1); if (length("`X'")>0) {; local error = 1; }; }; /* end for */ if `error' == 1 {; noisily display "ERROR: The correct answers you entered for Form `Z' are invalid: "; noisily display " Possible correct answers are some combination of: A B C D E (case doesn't matter)."; noisily display " Fix your inputs in Stata do-file and re-run."; }; /* end if */ }; /* end if */ }; /* end for */ if `error' == 1 {; exit; }; /* Identify valid form codes */ local forms ; foreach Z in A B C D {; local W : word count `an`Z''; if `W'==`numq' {; local forms = trim("`forms' `Z'"); }; }; if `numf' > 1 {; local def_form = trim(substr("`forms'",1,index("`forms'"," "))); /* Default form code for students that left it blank */ }; else if `numf' == 1 {; local def_form = "`forms'"; /* Default form code for students that left it blank */ }; /* Fill in point values for each question if user is using equal point values across questions */ foreach Z in A B C D {; if ("`pt`Z''" == "" & "`pts'"~="" & index("`forms'","`Z'")~=0) {; local pt`Z' `pts'; forvalues X = 2(1)`numq' {; local pt`Z' "`pt`Z'' `pts'"; }; }; /* end if */ }; /* end for */ if ("`pts'"~="") {; if (`pts' <= 0) {; noisily display "ERROR: Points awarded for each question pts (=`pts') must be > 0"; noisily display " Fix Stata do-file and re-run."; exit; }; /* end if */ }; /* end if */ if ("`min_pts'"=="") {; local min_pts = 0; }; /* Make sure that point values for each question have been fully specified by user */ local i = 0; foreach Z in A B C D {; local W : word count `pt`Z''; if `W' >= 1 {; local i = `i' + 1; }; }; if (`i' < `numf' & `i'~=0) {; noisily display "ERROR: You have failed to enter point values for each question for each form"; noisily display " Fix Stata do-file and re-run."; exit; }; /* end if */ if (`i'==0 & "`pts'"=="") {; noisily display "ERROR: You have failed to enter point values for each question (either fill"; noisily display " in pts with a value if all questions worth same value or fill in the"; noisily display " correct question-by-question values."; noisily display " Fix Stata do-file and re-run."; exit; }; /* end if */ if (`numf' < 1 | `numf' > 4) {; noisily display "ERROR: Number of forms numf (=`numf') must be >= 1 and <= 4"; noisily display " Fix Stata do-file and re-run."; exit; }; /* end if */ foreach Z in A B C D {; local W : word count `pt`Z''; if (`W'~=`numq' & `W'~=0) {; noisily display "ERROR: # of point values you entered for Form `Z' (=`W') not equal to # of"; noisily display " questions (=`numq'). Fix your inputs in Stata do-file and re-run."; local error = 1; }; /* end if */ }; /* end for */ if `error' == 1 {; exit; }; local i = 0; foreach Z in A B C D {; local W : word count `qo`Z''; local i = `i' + `W'; if (`W'~=`numq' & `W'~=0) {; noisily display "ERROR: # of question numbers (=`W') you entered for the question ordering of Form `Z'"; noisily display " not equal to # of questions (=`numq')"; noisily display " Fix your inputs in Stata do-file and re-run."; local error = 1; }; /* end if */ }; /* end for */ if `error' == 1 {; exit; }; if `i' == 0 {; local base ; /* Ensures base form is blank if the user is not using the option to match questions across forms */ }; local terror = 0; foreach Z in A B C D {; local i = 0; local tt`Z' `qo`Z''; forvalues Y = 1(1)`numq' {; local tt`Z' : subinstr local tt`Z' "`Y'" " ", word; /* works b/c only changes 1st occurrence */ }; local W : word count `tt`Z''; if (`W'~=0) {; local terror = 1; local error = 1; }; /* end if */ if `terror' == 1 {; noisily display "ERROR: The question ordering you entered for Form `Z' (qo`Z') does not make sense."; noisily display " Each number from 1 to `numq' must appear exactly once."; noisily display " Fix in Stata do-file and re-run"; }; local terror = 0; }; /* end for */ if `error' == 1 {; exit; }; if ("`base'"~="A" & "`base'"~="B" & "`base'"~="C" & "`base'"~="D" & "`base'"~="") {; noisily display "ERROR: Your entry for base form (=`base') is invalid: must be A, B, C, D or left blank."; noisily display " Fix in Stata do-file and re-run"; exit; }; /* end if */ /* Check that total points possible is equal across forms */ local tpts = 0; forvalues Y = 1(1)`numq' {; local W : word `Y' of `pt`def_form''; local tpts = `tpts' + `W'; }; foreach Z in A B C D {; local tpts`Z' = 0; forvalues Y = 1(1)`numq' {; local W : word `Y' of `pt`Z''; if "`W'"~="" {; local tpts`Z' = `tpts`Z'' + `W'; }; }; if (`tpts`Z''~=`tpts' & `tpts`Z''~=0) {; local terror = 1; local error = 1; }; if `terror' == 1 {; noisily display "ERROR: The points you entered for Form `Z' (pt`Z') total to a different value (=`tpts`Z'')"; noisily display " than Form `def_form' (=`tpts'). Fix in Stata do-file and re-run"; }; local terror = 0; }; /* end for */ if `error' == 1 {; exit; }; if (index("`forms'","`base'")==0 & wordcount("`forms'")>1) {; noisily display "ERROR: Your entry for base form (=`base') is invalid."; noisily display " Fix in Stata do-file and re-run"; exit; }; /* end if */ /* Make sure that minimum point value for each question less than the value of getting the correct answer for every question */ forvalues Y = 1(1)`numq' {; local W : word `Y' of `pt`def_form''; if `min_pts' >= `W' {; local error = 1; }; }; if `error' == 1 {; noisily display "ERROR: The minimum points per question cannot be greater than"; noisily display " or equal to the points awarded for a correct answer (which"; noisily display " could penalize students for getting it right). Fix your"; noisily display " point-value inputs in Stata do-file and re-run."; exit; }; /* Summarize different point values used across questions for display in the report */ local list_pts = ""; forvalues Y = 1(1)`numq' {; local W : word `Y' of `pt`def_form''; if (index(" `list_pts'", " `W' ")==0) {; local list_pts = "`list_pts'" + "`W' "; }; }; local list_pts = trim("`list_pts'"); local list_pts = subinstr("`list_pts'"," "," or ",20); if (`flat' < 0 | `flat' > 50) {; noisily display "ERROR: Your entry for flat curve (=`flat') is invalid. Must be >=0 and <= 50."; noisily display " (If you don't want a curve, enter 0). Fix in Stata do-file and re-run."; exit; }; /* end if */ if (`prop' < 0 | `prop' > 50) {; noisily display "ERROR: Your entry for proportional curve (=`prop') is invalid. Must be >=0 and <= 50."; noisily display " (If you don't want a curve, enter 0). Fix in Stata do-file and re-run."; exit; }; /* end if */ /*** Input file containing data from the scanned forms ***/ clear; capture insheet using `scan', nonames comma; if _rc ~= 0 {; noisily display "ERROR: The file will scanned papers (name = `scan') has not been found in the folder:"; noisily display " " c(pwd); noisily display " Exit Stata, fix, and then double click the Stata do-file to re-run it."; exit; }; /* end if */ describe; if r(k) ~= (`numq' + 4) {; noisily display "ERROR: The file `scan' with the scanned student answers does not"; noisily display " appear to be in the correct format. The first four columns should"; noisily display " be last name, initial, form code, and student #. Then there should"; noisily display " be `numq' columns containing the students' answers to the questions."; exit; }; /* end if */ rename v1 lname_scan; label var lname_scan "Student's last name as entered on bubble form"; rename v2 initial_scan; label var initial_scan "Student's initial as entered on bubble form"; rename v3 form; label var form "FORM CODE as entered on bubble form"; rename v4 student_id; label var student_id "Student's student id number as entered on bubble form"; format student_id %10.0f; forvalues X = 1(1)`numq' {; local Y = `X' + 4; /* Last name, initial, form, student id are 1st 4 variables*/ rename v`Y' q`X'; label var q`X' "Student's letter answer to Ques #`X'"; }; /* Verify student_id numeric */ capture replace student_id = student_id + 0; if _rc ~= 0 {; noisily display "ERROR: Student id number should be fourth field in `scan' and"; noisily display " must be in numeric format. Make sure there is no header row"; noisily display " in the file: 1st row should be 1st student. Fix the file and"; noisily display " re-run Stata do-file."; exit; }; /* end if */ /* Verify that there are no missing or definitely invalid student numbers */ count if (student_id > 9999999 & student_id < 9999999999); local pot_valid_ids = r(N); count; if r(N) ~= `pot_valid_ids' {; noisily display "ERROR: There are invalid student numbers in the scanned data file (`scan')."; noisily display " These are listed below."; noisily display " Fix the scanned data file and re-run Stata do-file."; noisily display ""; noisily display "Scanned last name, scanned first initial, scanned student number of problematic records:"; noisily display "A missing student number appears as ."; noisily list lname_scan initial_scan student_id if (student_id <= 9999999 | student_id >= 9999999999), noheader; exit; }; /* Verify that there are no duplicate student numbers */ sort student_id; by student_id: gen n_id = _N; egen n_id_vals = sum(n_id); /* Note: This will total number of ids if each occurs once (correct) but will simply be a too big # otherwise */ count; if r(N) ~= n_id_vals {; noisily display "ERROR: There are multiple records for the same student number in"; noisily display " the scanned data (`scan'). These are listed below."; noisily display " Fix the scanned data file and re-run Stata do-file."; noisily display ""; noisily display "Scanned last name, scanned first initial, scanned student number of problematic records:"; noisily list lname_scan initial_scan student_id if n_id > 1, noheader; exit; }; drop n_id n_id_vals; capture replace lname_scan = upper(trim(lname_scan)); if _rc ~= 0 {; noisily display "ERROR: Student last name should be first field in `scan' and"; noisily display " must be in character format. Fix the file and re-run Stata do-file."; exit; }; /* end if */ capture replace initial_scan = upper(trim(initial_scan)); if _rc ~= 0 {; noisily display "ERROR: Student initial should be second field in `scan' and"; noisily display " must be in character format. Fix the file and re-run Stata do-file."; exit; }; /* end if */ gen form_`suff' = trim(upper(form)); gen form_prob_`suff' = 0; replace form_prob_`suff' = 1 if (strpos("`forms'",form_`suff'[_n])==0 | form_`suff'[_n]==""); label var form_prob_`suff' "=1 if student entered an invalid form code and hence is graded according to `def_form'"; replace form_`suff' = "`def_form'" if form_prob_`suff'==1; label var form_`suff' "Version of Test (FORM) (equals `def_form' for students that entered an invalid form code)"; count if form_prob_`suff'==1; local form_prob = r(N); if `form_prob'~=0 {; noisily display "WARNING: `form_prob' student(s) have an invalid FORM CODE in `scan'"; noisily display " Valid forms: `forms'. Anything else is treated as form `def_form'."; }; /* end if */ foreach X of var q* {; replace `X' = upper(`X'); replace `X' = "" if (`X'~="A" & `X'~="B" & `X'~="C" & `X'~="D" & `X'~="E"); }; /*** Mark Answers ***/ forvalues Y = 1(1)`numq' {; gen p_`Y' = `min_pts'; /* Will be replaced with points earned by student on that question for a correct answer */ label var p_`Y' "= points student earned on Ques #`Y'"; }; foreach Z in `forms' {; forvalues Y = 1(1)`numq' {; local X : word `Y' of `an`Z''; /* Correct letter answer(s) for this question */ local W : word `Y' of `pt`Z''; /* Points for correct answer for this question */ replace p_`Y' = `W' if (index("`X'",q`Y')~=0 & q`Y'~="" & form_`suff'=="`Z'"); /* Checks if student selected any of the correct answers */ /* Make sure that if accept all answers for a question, also accept blank */ replace p_`Y' = `W' if (index("`X'","A")~=0 & index("`X'","B")~=0 & index("`X'","C")~=0 & index("`X'","D")~=0 & index("`X'","E")~=0 & form_`suff'=="`Z'"); }; }; quietly forvalues X = 1(1)`numq'{; replace q`X' = lower(q`X') if (p_`X'==`min_pts'); }; gen num_blank_`suff' = 0; forvalues Y = 1(1)`numq' {; replace num_blank_`suff' = num_blank_`suff' + 1 if q`Y'==""; }; label var num_blank_`suff' "num_blank: Number of questions student left blank on `suff'"; gen raw_pts_`suff' = 0; forvalues Y = 1(1)`numq' {; replace raw_pts_`suff' = raw_pts_`suff' + p_`Y'; }; label var raw_pts_`suff' "Total points earned out of `tpts' possible on `suff'"; gen raw_pct_`suff' = 100*raw_pts_`suff'/`tpts'; label var raw_pct_`suff' "% Mark before any curve (=100*points earned/`tpts') on `suff'"; gen pct_mark_`suff' = (100*raw_pts_`suff'/(`tpts') + `flat') * (100 + `prop')/100; local mult = (100 + `prop')/100; if (`flat'==0 & `prop'==0) {; label var pct_mark_`suff' "% Mark (no curve applied) on `suff'"; }; else if (`flat'~=0 & `prop'==0) {; label var pct_mark_`suff' "% Mark Curved (100*points/`tpts' + `flat') on `suff'"; }; else if (`flat'==0 & `prop'~=0) {; label var pct_mark_`suff' "% Mark Curved (`mult'*100*points/`tpts') on `suff'"; }; else if (`flat'~=0 & `prop'~=0) {; label var pct_mark_`suff' "% Mark Curved (`mult'*(`flat' + 100*points/`tpts')) on `suff'"; }; /*** Check that bubble forms match up with list of registered students ***/ sort student_id; save `scanned'; clear; capture insheet using `rosi', nonames comma; if _rc ~= 0 {; noisily display "ERROR: The file with your class list (name = `rosi') has not been found in the folder:"; noisily display " " c(pwd); noisily display " Exit Stata, fix, and then double click the Stata do-file to re-run it."; exit; }; /* end if */ describe; if r(k) < 3 {; noisily display "ERROR: The file `rosi' with the registered students does not"; noisily display " appear to be in the correct format. It must include at least"; noisily display " 3 fields: first name, last name, student #. Please get it into"; noisily display " the format specified in the Stata do-file and then re-run it."; exit; }; /* end if */ rename v1 fname_rosi; rename v2 lname_rosi; rename v3 student_id; label var student_id "Student identification number"; label var lname_rosi "Student's last name (as registered)"; label var fname_rosi "Student's first name (as registered)"; local nums = 0; if r(k) > 3 {; gen section = ""; capture replace section = trim(string(v4,"%12.0f")); capture replace section = v4; replace section = subinstr(section," ","_",10); label var section "Section of student"; drop v4; sort section; local obs = _N; local sections = ""; forvalues X = 1(1)`obs' {; local W = section[`X']; if index("`sections'","`W'")==0 {; local sections "`sections' `W'"; local nums = `nums' + 1; }; }; if `nums' > 10 {; noisily display "ERROR: Is the fourth column of your class list file (name = `rosi') the section of each student?"; noisily display "It seems to have too many unique values (`nums'). The class list file must have first name, last name,"; noisily display "student id number, and then section of each student (fourth column). You may leave the fourth"; noisily display "column blank if you do not wish to break out your results separately for each section."; exit; }; }; /* Verify student_id numeric */ capture replace student_id = student_id + 0; if _rc ~= 0 {; noisily display "ERROR: Student number should be third field in `rosi' and"; noisily display " must be in numeric format. Make sure there is no header row"; noisily display " in the file: 1st row should be 1st student. Fix the file and"; noisily display " re-run Stata do-file."; exit; }; /* end if */ sort student_id; merge student_id using `scanned'; rename _merge merge_`suff'; label var merge_`suff' "Merge ROSI and bubble forms (1: rosi only, 2: scanned data only, 3: both)"; noisily tab merge_`suff'; /* Create short student id (last several numbers) for public posting */ gen temp_id = string(student_id,"%10.0f"); gen short_id = temp_id; foreach X in 7 6 5 4 3 {; gen last`X'_id = substr(temp_id, length(temp_id)-`X'+1,`X'); sort last`X'_id; quietly by last`X'_id: gen temp`X' = _N; replace short_id = last`X'_id if temp`X'==1; }; drop temp* last7_id last6_id last5_id last4_id; /* Make file to privatize the student_id numbers for publically posted files */ replace last3_id = "_" + last3_id; replace short_id = "_" + short_id; gen short_id_star = "*" + last3_id if short_id~=last3_id; sort last3_id short_id; gen last3_id_star = last3_id; replace last3_id_star = short_id_star if short_id_star~=""; replace short_id = "" if index(short_id_star,"*")==0; drop short_id_star; rename last3_id last3_id_sort; rename last3_id_star last3_id; label var last3_id_sort "Last three digits of student id #"; label var last3_id "Last three digits of student id # and a * if NOT an unique identifier"; label var short_id "Minimum final digits of student id # that uniquely identify this student"; order student_id last3_id_sort last3_id short_id; gen priv_`suff' = 0; if "`priv'"~="" {; foreach X in `priv' {; replace priv_`suff' = 1 if student_id==`X'; }; }; label var priv_`suff' "=1 if student requested not to have mark posted on web"; count if priv_`suff' == 1; local num_privacy = r(N); gen excd_`suff' = 0; if "`excd'"~="" {; foreach X in `excd' {; replace excd_`suff'= 1 if student_id==`X'; }; }; label var excd_`suff' "=1 if you excused student for missing this test (`suff')"; count if excd_`suff' == 1; local num_excused = r(N); replace num_blank_`suff' = 0 if (num_blank_`suff'==. & excd_`suff'==0); replace raw_pct_`suff' = 0 if (raw_pct_`suff'==. & excd_`suff'==0); replace raw_pts_`suff' = 0 if (raw_pts_`suff'==. & excd_`suff'==0); replace pct_mark_`suff' = 0 if (pct_mark_`suff'==. & excd_`suff'==0); save `holder'; /*** Make spreadsheet containing students' information and marks FOR REGISTERED STUDENTS ONLY ***/ drop if merge_`suff'== 2; rename lname_rosi lname; rename fname_rosi fname; renpfix p_ pts_q; order lname fname student_id pct_mark_`suff' raw_pct_`suff' raw_pts_`suff' q* pts_* form* excd_`suff'; drop lname_scan initial_scan form num_blank* merge_* priv_*; sort lname fname student_id; outsheet using "mcq_marks_`suff'.csv", comma replace; /*** Make spreadsheet that you can post to your course web site that contains students' marks, answers, and student id number ***/ clear; use `holder'; drop if merge_`suff'== 2; /* Do not post marks for students who are not registered in course */ keep last3_id_sort last3_id short_id pct_mark_`suff' form_`suff' q* priv_`suff'; replace pct_mark_`suff' = round(pct_mark_`suff', 0.1); /* Exclude students who for privacy reasons do not want their marks and answers posted on the the course web site. */ drop if priv_`suff' == 1; drop priv_`suff'; sort last3_id_sort short_id; drop last3_id_sort; order last3_id short_id pct_mark_`suff' form_`suff' q*; outsheet using "mcq_marks_web_site_`suff'.csv", comma replace; /*** Make detailed report on student performance by question ***/ clear; use `holder'; capture log off; capture log close; gen name = subinstr(c(current_date)," ","_",.); local name = name[1]; drop name; log using "REPORT_`suff'.txt", text replace; log on; noisily display "Date of last revision to this software: April 11, 2014"; noisily display "http://homes.chass.utoronto.ca/~murdockj/teaching/#marking_software"; noisily display ""; noisily display c(current_date) " " c(current_time); noisily display ""; noisily display "Input file of scanned answers: `scan'"; noisily display ""; noisily display "Input file of registered students (ROSI): `rosi'"; noisily display ""; noisily display "Output file of students' marks: mcq_marks_`suff'.csv"; noisily display ""; noisily display "Output file (unformatted) to post results: mcq_marks_web_site_`suff'.csv"; noisily display ""; noisily display " > A formatted template for Excel that you may cut-and-paste into is available at:"; noisily display " http://homes.chass.utoronto.ca/~murdockj/teaching/mcq_marks_web_site.xlsx."; noisily display " Please use this template for posting your results for students: you may"; noisily display " convert it to a pdf file for maximum accessibility."; noisily display ""; noisily display "Output histograms: HISTOGRAM_`suff'.emf, HISTOGRAM_alt_`suff'.emf"; noisily display ""; noisily display " > Mathematically identical, choose the one that looks best."; noisily display " You may convert it to a pdf file for maximum accessibility."; noisily display ""; count if priv_`suff' == 1; if r(N)>0 {; noisily display "These " r(N) " students requested not to have their marks posted on course web site:"; sort lname_rosi fname_rosi student_id; noisily list lname_rosi fname_rosi student_id if priv_`suff'==1; noisily display "These are excluded from the file mcq_marks_web_site_`suff'.csv"; noisily display ""; }; /* end if */ if wordcount("`priv'")~=`num_privacy' {; noisily display "WARNING: Not all of the students you listed for privacy (entered in Stata do-file)"; noisily display " have been found among students registered in your class. Please check"; noisily display " that you did not incorrectly enter the student id # of a student."; noisily display " (The list above only includes students requesting privacy that ARE registered.)"; }; /* end if */ count if excd_`suff' == 1; noisily display "Following " r(N) " students are excused from this assessment:"; sort lname_rosi fname_rosi student_id; noisily list lname_rosi fname_rosi student_id if excd_`suff'==1; if wordcount("`excd'")~=`num_excused' {; noisily display "WARNING: Not all of the students you listed as excused (entered in Stata do-file)"; noisily display " have been found among students registered in your class. Please check"; noisily display " that you did not incorrectly enter the student id # of an excused student."; noisily display " (The list above only includes excused students that ARE registered.)"; }; /* end if */ noisily display ""; count if (merge_`suff'==1 & excd_`suff'==0); local num_missing = r(N); if `num_missing' ~= 0 {; noisily display "Following `num_missing' students are registered but are NOT in scanned data AND"; noisily display "have NOT been excused from the assessment: "; sort lname_rosi fname_rosi student_id; noisily list lname_rosi fname_rosi student_id if (merge_`suff'==1 & excd_`suff'==0), header; noisily display "Check that count of students who wrote the test matches number of records in"; noisily display "the scanned data. If records are missing, manually enter data in `scan'"; noisily display "and re-run this Stata do-file."; noisily display ""; }; /* end if */ count if merge_`suff'==2; local num_notreg = r(N); if `num_notreg' ~= 0 {; noisily display "Following `num_notreg' students appear in scanned data but NOT in list of registered students: "; sort lname_scan initial_scan student_id; noisily list lname_scan initial_scan student_id if merge_`suff'==2, header; noisily display ""; noisily display "Check if these students filled in their student id # correctly."; noisily display "If they did not, make corrections to `scan' and re-run Stata do-file."; noisily display ""; }; /* end if */ noisily display "Total number of questions in assessment: " "`numq'"; noisily display ""; noisily display "Number of different forms administered: " "`numf'"; noisily display ""; noisily display "Correct answers as you entered them in Stata do-file: "; noisily display ""; foreach Z in `forms' {; noisily display "FORM `Z': `an`Z''"; noisily display ""; }; foreach Z in `forms' {; forvalues Y = 1(1)`numq' {; local W : word `Y' of `an`Z''; if length("`W'") > 1 {; if length("`W'")==2 {; local T = substr("`W'",1,1) + " and " + substr("`W'",2,1); }; if length("`W'")==3 {; local T = substr("`W'",1,1) + ", " + substr("`W'",2,1) + " and " + substr("`W'",3,1); }; if length("`W'")==4 {; local T = substr("`W'",1,1) + ", " + substr("`W'",2,1) + ", " + substr("`W'",3,1) + " and " + substr("`W'",4,1); }; if length("`W'")==5 {; local T = substr("`W'",1,1) + ", " + substr("`W'",2,1) + ", " + substr("`W'",3,1) + ", " + substr("`W'",4,1) + " and " + substr("`W'",5,1); }; noisily display "You accepted `T' as correct answers for question `Y' on Form `Z'"; noisily display ""; }; }; }; noisily display "Total possible points: `tpts'"; noisily display ""; noisily display "Correct replies worth: `list_pts' points"; noisily display ""; noisily display "Incorrect (or blank) replies worth: `min_pts' points"; noisily display ""; noisily display "Curved percentage score = (percent mark + `flat') * (100 + `prop')/100"; noisily display ""; keep if merge_`suff' == 3; count; local num_in = r(N); noisily display "MARKS SUMMARY for `num_in' students that submitted a bubble form AND are registered:"; noisily su raw_pts_`suff', detail; noisily su pct_mark_`suff', detail; noisily tab pct_mark_`suff', missing; if `numf' > 1 {; foreach Z in `forms' {; noisily display ""; noisily display "Summary for Form `Z':"; noisily su pct_mark_`suff' if form=="`Z'", detail; }; }; if `nums' > 1 {; foreach Z in `sections' {; noisily display ""; noisily display "Summary for Section `Z':"; noisily su pct_mark_`suff' if section=="`Z'", detail; }; }; noisily tab form_`suff', missing; if `form_prob' ~= 0 {; noisily display ""; noisily display "WARNING: `form_prob' student(s) have an invalid FORM CODE in `scan'"; noisily display " They are graded according to form `def_form'."; }; /* end if */ save `sum'; if (`numf'>1 & "`base'"~="") {; local all = "ALL"; }; local explanation1 = 0; local explanation2 = 0; local explanation3 = 0; /* If user included a column for the lecture section but (1) there is only one section OR (2) there are multiple sections and multiple forms but they've not used question orderings then do not separately loop through the sections below. It is too much to do a summary by form, by section. */ local tsections = ""; if (wordcount("`sections'") > 1 & (`numf' == 1 | (`numf' > 1 & "`base'"~=""))) {; local tsections = "`sections'"; }; local form_count = 0; local all_ind = 0; local sec_ind = 0; foreach Z in `forms' `all' `tsections' {; use `sum', clear; if ("`Z'"~="ALL" & `form_count' < `numf') {; keep if form_`suff'=="`Z'"; local form_count = `form_count' + 1; local ZZ = "`Z'"; }; else if ("`Z'"=="ALL" | (`all_ind'==1 & `form_count' >= `numf')) {; local ZZ = "`Z'"; forvalues X = 1(1)`numq' {; gen t_q`X' = ""; gen t_p_`X' = .; foreach Q in `forms' {; local W : word `X' of `qo`Q''; replace t_q`X' = q`W' if form_`suff'=="`Q'"; replace t_p_`X' = p_`W' if form_`suff'=="`Q'"; }; }; drop q* p_*; renpfix t_; if `all_ind'==1 {; keep if section=="`Z'"; local ZZ = "ALL"; local sec_ind = 1; }; local all_ind = 1; }; else if ("`Z'"~="ALL" & `all_ind'==0 & `form_count' >= `numf' & `numf'==1) {; keep if section=="`Z'"; local ZZ = "`def_form'"; local sec_ind = 1; }; noisily display ""; if `sec_ind'==0 {; noisily display "SUMMARY FOR FORM `ZZ' (" _N " students): "; }; else if `sec_ind'==1 {; noisily display "SUMMARY FOR STUDENTS IN SECTION `Z' (FORM `ZZ') (" _N " students): "; }; noisily display ""; su raw_pts_`suff', detail; gen quartile = "1" if (raw_pts_`suff' < r(p25)); replace quartile = "2" if (raw_pts_`suff' >= r(p25) & raw_pts_`suff' <= r(p50)); replace quartile = "3" if (raw_pts_`suff' > r(p50) & raw_pts_`suff' <= r(p75)); replace quartile = "4" if (raw_pts_`suff' > r(p75) & raw_pts_`suff' ~= .); noisily display " Students divided into quartiles based on total points earned:"; noisily tab quartile; if `explanation1'==0 {; noisily display " Note: All students with same mark put in same quartile,"; noisily display " which implies deviations from expected 25% in each quartile."; local explanation1 = 1; }; save form, replace; forvalues X = 1(1)`numq' {; if "`ZZ'" ~= "ALL" {; local A_`X' : word `X' of `an`ZZ''; /* Correct letter answer(s) for this question */ local P_`X' : word `X' of `pt`ZZ''; /* Point value for this question */ }; if "`ZZ'" == "ALL" {; local A_`X' : word `X' of `an`base''; /* Correct letter answer(s) for this question */ local P_`X' : word `X' of `pt`base''; /* Point value for this question */ }; }; /* end for */ gen temp = 1; forvalues Y = 1(1)4 {; count if quartile == "`Y'"; local num_`Y' = r(N); }; forvalues X = 1(1)`numq' {; forvalues Y = 1(1)4 {; count if (p_`X'~=`min_pts' & quartile == "`Y'"); local pc_`Y'_`X' = 100*r(N)/`num_`Y''; /* Percent in that quartile getting the question correct */ }; count if (p_`X'~=`min_pts'); local pc_ALL_`X' = 100*r(N)/_N; local DI_`X' = `pc_4_`X'' - `pc_1_`X''; }; forvalues X = 1(1)`numq' {; gen raw_pct_ex`X' = 100*(raw_pts_`suff' - p_`X')/(`tpts' - `P_`X''); gen c_`X' = 0; replace c_`X' = 1 if p_`X' > `min_pts'; regress raw_pct_ex`X' c_`X'; local r2_`X' = e(r2)*100; corr raw_pct_ex`X' c_`X', cov; local cov_`X' = r(cov_12); su c_`X'; local sd_`X' = r(sd); local slope_`X' = `cov_`X''/`sd_`X''^2; }; forvalues X = 1(1)`numq' {; foreach Y in A B C D E {; count if upper(q`X')=="`Y'"; local s`X'`Y' = 100*r(N)/_N; }; count if q`X' == ""; local s`X'_ = 100*r(N)/_N; }; if `explanation2'==0 {; noisily display ""; noisily display " Summary of student performance by question for Form `ZZ':"; noisily display " Percent of students that selected the correct answer (column ALL)."; noisily display " Percent of students by quartile that selected the correct answer."; noisily display " Students put in quartiles (Q1 - Q4) by percent correct overall."; noisily display " Discrimination index (DI): difference between top and bottom"; noisily display " quartiles in percent of students choosing the correct answer."; noisily display " R-squared (r2): Percent of variation in students' % mark on all"; noisily display " other questions combined that is explained by whether or not"; noisily display " student got this question correct."; noisily display " Slope (slope): Tells how much higher on average the students'"; noisily display " % mark on all other questions combined is for students who got"; noisily display " this question correct."; noisily display " (For both r2 and slope, percentage mark on all other questions"; noisily display " combined excludes the question being analyzed.)"; local explanation2 = 1; }; clear; set obs `numq'; gen Ques = string(_n); replace Ques = " " + Ques if length(Ques)==1; foreach Y in ALL Q1 Q2 Q3 Q4 DI r2 slope sA sB sC sD sE s_ {; gen `Y' = .; }; gen Ans = ""; gen Pts = ""; forvalues X = 1(1)`numq' {; replace ALL = `pc_ALL_`X'' if _n==`X'; replace Q1 = `pc_1_`X'' if _n==`X'; replace Q2 = `pc_2_`X'' if _n==`X'; replace Q3 = `pc_3_`X'' if _n==`X'; replace Q4 = `pc_4_`X'' if _n==`X'; replace DI = `DI_`X'' if _n==`X'; replace r2 = `r2_`X'' if _n==`X'; replace slope = `slope_`X'' if _n==`X'; replace sA = `s`X'A' if _n==`X'; replace sB = `s`X'B' if _n==`X'; replace sC = `s`X'C' if _n==`X'; replace sD = `s`X'D' if _n==`X'; replace sE = `s`X'E' if _n==`X'; replace s_ = `s`X'_' if _n==`X'; replace Ans = "`A_`X''" if _n==`X'; replace Pts = "`P_`X''" if _n==`X'; }; format %6.0f ALL Q1 Q2 Q3 Q4 DI r2 slope sA sB sC sD sE s_; save res_table, replace; collapse (mean) ALL Q1 Q2 Q3 Q4 DI r2 slope sA sB sC sD sE s_; gen Ques = "mean"; gen Ans = "-"; gen Pts = "-"; append using res_table; sort Ques; save res_table, replace; noisily list Ques ALL Q1 Q2 Q3 Q4 DI r2 slope, header(10) noobs; if `explanation3'==0 {; noisily display ""; noisily display " Percent of students that selected the correct answer (column ALL)."; noisily display " Percent of students selecting A - E (sA - sE) or not making a choice (s_)."; noisily display " Correct answer(s) to question (Ans) and point value of question (Pts)."; local explanation3 = 1; }; noisily list Ques ALL sA sB sC sD sE s_ Ans Pts, header(10) noobs; if ("`base'"~="" & "`ZZ'"=="ALL") {; noisily display "You specified `base' as the base form."; foreach T in `forms' {; noisily display "Question ordering for Form `T':"; noisily display " `qo`T''"; }; /* end for */ }; /* Check that ordering of questions across forms has been entered correctly */ if "`ZZ'" == "ALL" {; gen user_error = 0; gen temp = 0; foreach V in A B C D E {; replace temp = temp + s`V' if index(Ans,"`V'")~=0; }; replace temp = temp + s_ if ALL==100; /* If all answers accepted then blanks are accepted */ replace user_error = 1 if (Ques~="mean" & (ALL > (temp + 0.01) | ALL < (temp - 0.01))); /* +/- 0.01 for machine precision errors */ drop temp; sort user_error; if user_error[_N] == 1 {; local error = 1; noisily display ""; noisily display "ERROR: There is an error in your specification of the question orderings"; noisily display " or the correct answers across forms. Check your entries in qo*"; noisily display " (question orderings) and an* (correct answers)."; noisily display " Fix Stata do-file and re-run."; exit; }; /* end if */ }; /* end if */ if `error'==1 {; exit; }; /* More detailed break-down of how different students select different answers */ if (("`Z'"=="ALL" | `numf'==1) & `sec_ind'==0 ){; /* Only do detail for overall analysis and not also by form or section */ use form, clear; forvalues X = 1(1)`numq' {; forvalues R = 1(1)4 {; count if quartile=="`R'"; local Q = r(N); foreach Y in A B C D E {; count if (upper(q`X')=="`Y'" & quartile=="`R'"); local s`X'`Y'`R' = 100*r(N)/`Q'; }; count if (q`X' == "" & quartile=="`R'"); local s`X'_`R' = 100*r(N)/`Q'; }; }; clear; set obs `numq'; gen Ques = string(_n); expand 4; sort Ques; quietly by Ques: gen Quartile = _n; foreach Y in sA sB sC sD sE s_ {; gen `Y' = .; }; gen Ans = ""; gen Pts = ""; forvalues X = 1(1)`numq' {; forvalues R = 1(1)4 {; replace sA = `s`X'A`R'' if (Ques=="`X'" & Quartile==`R'); replace sB = `s`X'B`R'' if (Ques=="`X'" & Quartile==`R'); replace sC = `s`X'C`R'' if (Ques=="`X'" & Quartile==`R'); replace sD = `s`X'D`R'' if (Ques=="`X'" & Quartile==`R'); replace sE = `s`X'E`R'' if (Ques=="`X'" & Quartile==`R'); replace s_ = `s`X'_`R'' if (Ques=="`X'" & Quartile==`R'); replace Ans = "`A_`X''" if (Ques=="`X'" & Quartile==`R'); replace Pts = "`P_`X''" if (Ques=="`X'" & Quartile==`R'); }; }; replace Ques = " " + Ques if length(Ques)==1; format %6.0f sA sB sC sD sE s_; sort Ques Quartile; order Ques Quartile sA sB sC sD sE s_ Ans Pts; noisily display ""; noisily display " More detailed summary of student performance by question for Form `Z':"; noisily list Ques Quartile sA sB sC sD sE s_ Ans Pts, noobs separator(4) header(12); }; /* end if */ if `error'==1 {; exit; }; }; /* end for */ if `error'==1 {; exit; }; log off; log close; use `sum', clear; su pct_mark_`suff', detail; local n = _N; local mean = string(r(mean),"%3.1f"); local median = string(r(p50),"%3.1f"); local sd = string(r(sd),"%3.1f"); local fsize large; histogram pct_mark_`suff', bin() start(0) fraction xlabel(,labsize(`fsize')) ylabel(,labsize(`fsize') angle(360)) xtitle(, size(`fsize')) ytitle(, size(`fsize')) title("`n' Marked Bubble Forms" , size(`fsize') color(black)) subtitle("mean: `mean', sd: `sd', median: `median'" , size(`fsize') color(black)) saving(HISTOGRAM_`suff', replace); graph export HISTOGRAM_`suff'.emf, replace; histogram pct_mark_`suff', bin() discrete start(0) fraction xlabel(,labsize(`fsize')) ylabel(,labsize(`fsize') angle(360)) xtitle(, size(`fsize')) ytitle(, size(`fsize')) title("`n' Marked Bubble Forms" , size(`fsize') color(black)) subtitle("mean: `mean', sd: `sd', median: `median'" , size(`fsize') color(black)) saving(HISTOGRAM_alt_`suff', replace); graph export HISTOGRAM_alt_`suff'.emf, replace; window manage close graph; drop _all; set more on; erase form.dta; /* /* This creates a more concise spreadsheet summary of student performance by question for cases where there is only one form (one version) in use or multiple versions where the question ordering is simply scrambled. In the later case it reports for all versions combined with the question ordering of the base form. */ if (`numf'==1 | (`numf'>1 & "`base'"~="")) {; use res_table.dta, clear; order Ques ALL Q1 Q2 Q3 Q4 DI r2 slope sA sB sC sD sE s_ Ans Pts; foreach X of var ALL Q1 Q2 Q3 Q4 DI r2 slope sA sB sC sD sE s_ {; replace `X' = round(`X', 1); }; outsheet using "question_analysis_ALL_orderform`base'_`suff'.csv", comma replace; }; */ erase res_table.dta; use `holder', clear; order student_id last3_id_sort last3_id short_id lname_rosi fname_rosi lname_scan initial_scan form_`suff' form form_prob_`suff' pct_mark_`suff' raw_pct_`suff' raw_pts_`suff' num_blank_`suff' merge_`suff' excd_`suff' priv_`suff'; /* End quietly */ }; exit;