diff --git a/extras/scripts/attendance.py b/extras/scripts/attendance.py new file mode 100644 index 00000000..2c66c49c --- /dev/null +++ b/extras/scripts/attendance.py @@ -0,0 +1,118 @@ +# this is a script rather than a notebook to avoid private student information being committed to the repository + +import pandas as pd + +NUM_CLASSES = 7 +FREEBIES = 1 +TOP_SCORE = NUM_CLASSES - FREEBIES + +ROLL_CALL_CSV = ( + "~/Downloads/attendance_reports_attendance-264e4d14-1765-4396-b311-4d927b59566d.csv" +) +# get by clicking into the Assignment and getting from the URL +ASSIGNMENT_ID = 1405957 +GRADEBOOK_FILE = "attendance.csv" +STUDENT_UNIQUE_COLS = ["Student ID", "Student Name", "Section Name", "Section"] + + +def normalize_sections(entries: pd.DataFrame): + """For students who switch sections, use their final section.""" + + student_info = entries.drop_duplicates(subset=["Student ID"], keep="last") + student_info = student_info[["Student ID", "Section Name"]] + + entries = entries.drop(columns=["Section Name"]) + return entries.merge(student_info, on="Student ID") + + +def get_entries(filename: str): + entries = pd.read_csv( + filename, + index_col=False, + usecols=[ + "Section Name", + "Student Name", + "Student ID", + "Class Date", + "Attendance", + ], + parse_dates=["Class Date"], + ) + + entries = normalize_sections(entries) + # pull the section number out + entries["Section"] = ( + entries["Section Name"].str.extract(r"INAFU6504_(\d{3})_").astype(int) + ) + + return entries + + +def print_heading(text: str): + print(f"-------------------\n\n{text.upper()}:\n") + + +def print_students(students: pd.Series): + print(students.droplevel(["Student ID", "Section Name"])) + print() + + +def validate(entries: pd.DataFrame): + recording_counts = entries.groupby(STUDENT_UNIQUE_COLS).size() + print_heading("Students missing entries") + print_students(recording_counts[recording_counts < NUM_CLASSES]) + + total_classes = entries["Class Date"].nunique() + assert total_classes == NUM_CLASSES + + +def compute_scores(entries: pd.DataFrame): + attended = entries[entries["Attendance"] == "present"] + attendance_counts = attended.groupby(STUDENT_UNIQUE_COLS).size() + # print_heading("Attendance counts") + # print_students(attendance_counts) + + # cap the top scores + attendance_counts[attendance_counts > TOP_SCORE] = TOP_SCORE + + return attendance_counts + + +def write_canvas_csv(scores: pd.Series): + """https://community.canvaslms.com/t5/Instructor-Guide/How-do-I-import-grades-in-the-Gradebook/ta-p/807""" + + attendance_col = f"Attendance ({ASSIGNMENT_ID})" + + gradebook = ( + scores.reset_index(name=attendance_col) + .drop(columns=["Section"]) + .rename( + columns={ + # Roll Call has `FIRST LAST`, gradebook has `LAST, FIRST`. Shouldn't matter. + "Student Name": "Student", + "Student ID": "ID", + "Section Name": "Section", + } + ) + ) + gradebook.to_csv(GRADEBOOK_FILE, index=False) + + print(f"Now upload {GRADEBOOK_FILE} to CourseWorks Gradebook.") + + +def run(): + entries = get_entries(ROLL_CALL_CSV) + validate(entries) + + scores = compute_scores(entries) + print_heading("Scores") + print_students(scores) + + lowered_scores = scores[scores < TOP_SCORE] + print_heading(f"Scores below {TOP_SCORE}") + print_students(lowered_scores.sort_values()) + + write_canvas_csv(scores) + + +run() diff --git a/meta/assistant_guide.md b/meta/assistant_guide.md index a263dd92..6c8f092a 100644 --- a/meta/assistant_guide.md +++ b/meta/assistant_guide.md @@ -215,3 +215,14 @@ It isn't your responsibility to look for potential instances of cheating/plagiar 1. In the Gradebook, give points to the reviewer under the Final Project Peer Review. [Scoring details.](../syllabus.md#final-project) + +{% if id == "columbia" -%} +## Final grades + +To compute the [attendance](../syllabus.md#attendance) score: + +1. [Export the Roll Call attendance data.](https://community.canvaslms.com/t5/Canvas-Basics-Guide/What-is-the-Roll-Call-Attendance-Tool/ta-p/59#export_attendance_data) +1. Copy [the script](https://github.com/afeld/python-public-policy/blob/main/extras/scripts/attendance.py) into {{coding_env_name}}. +1. Adjust the constants at the top as necessary. +1. Run the code. +{%- endif %}