import json from pprint import pprint from random import shuffle, randint from collections import Counter from difflib import SequenceMatcher from django.core.management.base import BaseCommand from university.models import OnlineExamQuestion PCTA_LESSON_WITH_PERIODS = { 'pcta-1-2': 1, 'pcta-2-1': 1, 'pcta-2-2': 2, 'pcta-3-1': 1, 'pcta-3-2': 1, 'pcta-6-1': 2, 'pcta-7-1': 1, 'pcta-7-2': 1, 'pcta-7-3': 1, 'pcta-8-1': 1, 'pcta-8-2': 1 } PCTP_LESSON_WITH_PERIODS = { 'pctp-1-1': 1, 'pctp-1-4': 1, 'pctp-1-5': 1, 'pctp-2-1': 1.5, 'pctp-2-2': 1, 'pctp-2-3': 1, 'pctp-3-1': 0.5, 'pctp-3-2': 1, 'pctp-3-3': 1, 'pctp-3-4': 1, 'pctp-3-5': 1, 'pctp-4-1': 0.5, 'pctp-4-2': 2, 'pctp-5-1': 1, 'pctp-5-2': 1.5, 'pctp-5-3': 2.5, 'pctp-6-1': 1.5, 'pctp-6-2': 1, 'pctp-6-3': 1, 'pctp-6-4': 1, 'pctp-6-5': 1, 'pctp-6-6': 1, 'pctp-6-7': 2, 'pctp-7-1': 1 } """ 1. PCTA 60 道,PCTP 70 道,按课时平分 2. 难度系数 1~3(含 3):60% 4~5(含 4):40% 3. PCTA 单选题 30、多选题 30, PCTP 单选27 多选23 不定项 20 """ def similarity(a, b): return SequenceMatcher(None, a, b).ratio() class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( '--exam_tag', required=True, dest='exam_tag' ) def handle(self, *args, **options): exam_tag = options['exam_tag'] total_question_nums = 0 lesson_with_periods = None if exam_tag == 'pcta': total_question_nums = 60 lesson_with_periods = PCTA_LESSON_WITH_PERIODS else: total_question_nums = 70 lesson_with_periods = PCTP_LESSON_WITH_PERIODS while True: selected_questions, lesson_question_num_expect_map = self.generate_exam(exam_tag, total_question_nums, lesson_with_periods) print('\033[32m难度系数分布:\033[0m') difficulty_level_distribution = Counter(['1~3' if q['difficulty_level'] <= 3 else '4~5' for q in selected_questions]) for k, v in difficulty_level_distribution.items(): print('{}: {}, {}%'.format(k, v, round(v / len(selected_questions) * 100, 2))) print('\033[32m每节课的题数分布:\033[0m') lesson_question_nums_distribution = { key: [value, lesson_question_num_expect_map[key]] for key, value in Counter([q['tag'] for q in selected_questions]).items() } for k, v in lesson_question_nums_distribution.items(): print('{}: 实际 {}道, 期望 {}道'.format(k, v[0], v[1])) type_count = Counter([q['question_type'] for q in selected_questions]) print(type_count) print(len(selected_questions)) above_all_count = 0 for q in selected_questions: for o in q['body']: for l in ['以上都', '以上全', '以上均', '以上选项']: if l in o['text']: above_all_count += 1 break else: continue break print('\033[32m“以上都”的数量:\033[0m {}'.format(above_all_count)) problem_similarity_flag = False for index_a, q1 in enumerate(selected_questions): for index_b, q2 in enumerate(selected_questions): if index_a != index_b and similarity(q1['problem'], q2['problem']) > 0.9: print("相似: similarity('{}', '{}')".format(q1['problem'], q2['problem'])) problem_similarity_flag = True break else: continue break if problem_similarity_flag: continue if exam_tag == 'pcta' and above_all_count < 4 and type_count[0] > 23 and type_count[1] > 23: break if exam_tag == 'pctp' and above_all_count < 5 and type_count[0] > 30 and type_count[1] > 30: break if exam_tag == 'pcta': self.dump_pcta_exam(selected_questions) else: self.dump_pctp_exam(selected_questions) def generate_exam(self, exam_tag, total_question_nums, lesson_with_periods, **options): lesson_question_num_expect_map = {} single_choice_expect_num = int(total_question_nums / 2) mutiple_choice_expect_num = int(total_question_nums / 2) low_difficulty_level_expect_num = int(total_question_nums * 0.8) high_difficulty_level_expect_num = int(total_question_nums * 0.2) single_choice_num = 0 mutiple_choice_num = 0 low_difficulty_level_num = 0 high_difficulty_level_num = 0 total_periods = sum(lesson_with_periods.values()) lessons = list(lesson_with_periods.keys()) shuffle(lessons) per_lesson_question_num = total_question_nums // (total_periods * 2) remain_question_num = total_question_nums - per_lesson_question_num * total_periods * 2 _compensate_num = remain_question_num // len(lessons) + 1 for t in lessons: if remain_question_num > 0: if remain_question_num >= _compensate_num: _compensate_num = _compensate_num else: _compensate_num = remain_question_num lesson_question_num_expect_map[t] = int( per_lesson_question_num * lesson_with_periods[t] * 2 + _compensate_num) remain_question_num -= _compensate_num else: lesson_question_num_expect_map[t] = int( per_lesson_question_num * lesson_with_periods[t] * 2) all_questions = list(OnlineExamQuestion.objects.filter(tag__icontains=exam_tag).values( 'id', 'question_type', 'problem', 'body', 'answer', 'difficulty_level', 'tag')) shuffle(all_questions) selected_questions = [] for q in all_questions: if q['difficulty_level'] <= 3 and low_difficulty_level_num < low_difficulty_level_expect_num: selected_questions.append(q) low_difficulty_level_num += 1 elif q['difficulty_level'] >= 4 and high_difficulty_level_num < high_difficulty_level_expect_num: selected_questions.append(q) high_difficulty_level_num += 1 else: pass selected_question_ids = [q['id'] for q in selected_questions] unselected_questions = list(filter(lambda q: q['id'] not in selected_question_ids, all_questions)) lesson_question_nums = { key: [value, lesson_question_num_expect_map[key]] for key, value in Counter([q['tag'] for q in selected_questions]).items() } attemp_times = 50 while attemp_times > 0: attemp_times -= 1 large_lesson = None small_lesson = None additional_num = 0 less_num = 0 handle_question_num = 0 for lesson, nums in lesson_question_nums.items(): if nums[0] - nums[1] >= 2: large_lesson = lesson additional_num = nums[0] - nums[1] elif nums[1] - nums[0] >= 2: small_lesson = lesson less_num = nums[1] - nums[0] else: pass if large_lesson and small_lesson: def _test(q): if q['tag'] != large_lesson: return False if q['difficulty_level'] > 3: return randint(0, 2) == 0 return True new_questions = list(filter(lambda q: q['tag'] == small_lesson, unselected_questions)) exit_questions = list(filter(_test, selected_questions)) handle_question_num = min(len(new_questions), len(exit_questions), additional_num, less_num) print(large_lesson, small_lesson, handle_question_num) new_questions = new_questions[:handle_question_num] new_question_ids = [q['id'] for q in new_questions] exit_questions = exit_questions[:handle_question_num] exit_question_ids = [q['id'] for q in exit_questions] print(large_lesson, small_lesson, handle_question_num) selected_question_ids = list(filter(lambda pk: pk not in exit_question_ids, selected_question_ids)) selected_question_ids += new_question_ids selected_questions = list(filter(lambda q: q['id'] in selected_question_ids, all_questions)) unselected_questions = list(filter(lambda q: q['id'] not in selected_question_ids, all_questions)) lesson_question_nums = { key: [value, lesson_question_num_expect_map[key]] for key, value in Counter([q['tag'] for q in selected_questions]).items() } return selected_questions, lesson_question_num_expect_map def dump_pctp_exam(self, selected_questions): pctp_structures = [{ 'title': '一、单选题', 'description': '', 'number_of_questions': 27, 'questions': [] }, { 'title': '二、多选题', 'description': '', 'number_of_questions': 23, 'questions': [] }, { 'title': '二、不定项题', 'description': '', 'number_of_questions': 20, 'questions': [] }] single_choice_questions = [{ 'id': str(q['id']), 'type': q['question_type'], 'score': 1, 'problem': q['problem'], 'options': q['body'] } for q in filter(lambda q: q['question_type'] == 0, selected_questions)] mutiple_choice_questions = [{ 'id': str(q['id']), 'type': q['question_type'], 'score': 1, 'problem': q['problem'], 'options': q['body'] } for q in filter(lambda q: q['question_type'] == 1, selected_questions)] other_type_questions = single_choice_questions[27:] + mutiple_choice_questions[23:] single_choice_questions = single_choice_questions[:27] mutiple_choice_questions = mutiple_choice_questions[:23] for q in other_type_questions: q['type'] = 1 pctp_structures[0]['questions'] = single_choice_questions pctp_structures[1]['questions'] = mutiple_choice_questions pctp_structures[2]['questions'] = other_type_questions with open('pctp_exam.json', 'w') as f: f.write(json.dumps(pctp_structures)) def dump_pcta_exam(self, selected_questions): pcta_structures = [{ 'title': '一、单选题', 'description': '', 'number_of_questions': 20, 'questions': [] }, { 'title': '二、多选题', 'description': '', 'number_of_questions': 20, 'questions': [] }, { 'title': '二、不定项题', 'description': '', 'number_of_questions': 20, 'questions': [] }] single_choice_questions = [{ 'id': str(q['id']), 'type': q['question_type'], 'score': 1, 'problem': q['problem'], 'options': q['body'] } for q in filter(lambda q: q['question_type'] == 0, selected_questions)] mutiple_choice_questions = [{ 'id': str(q['id']), 'type': q['question_type'], 'score': 1, 'problem': q['problem'], 'options': q['body'] } for q in filter(lambda q: q['question_type'] == 1, selected_questions)] other_type_questions = single_choice_questions[20:] + mutiple_choice_questions[20:] single_choice_questions = single_choice_questions[:20] mutiple_choice_questions = mutiple_choice_questions[:20] for q in other_type_questions: q['type'] = 1 pcta_structures[0]['questions'] = single_choice_questions pcta_structures[1]['questions'] = mutiple_choice_questions pcta_structures[2]['questions'] = other_type_questions with open('pcta_exam.json', 'w') as f: f.write(json.dumps(pcta_structures))