Fall 2026 Midterm 1: Log Analysis and NLP Pipeline¶

Version 2.2.2 All of the header information is important. Please read it.

Topics and number of exercises:** This problem builds on your knowledge of log analysis, NLP, sentiment analysis, regex. It has 9** exercises numbered 0 to 8. There are 17 available points. However to earn 100% the threshold is 12 points. (Therefore once you hit 12 points you can stop. There is no extra credit for exceeding this threshold.)

Exercise ordering:** Each exercise builds logically on previous exercises but you may solve them in any order. That is if you can't solve an exercise you can still move on and try the next one. Use this to your advantage as the exercises are **not necessarily ordered in terms of difficulty. Higher point values generally indicate more difficult exercises.

"Run Me" cells: Code cells starting with the comment ### Run Me!!! load pre-processed objects used to further the analysis problem. Make sure to execute them so that all the demonstrations in this notebook will work properly.

Demos: Each exercise will be accompanied by a short example demonstrating expected input and output format. By their nature, these examples are simplified and will not necessarily cover all possible scenarios. In other words, it is necessary to get the correct demo result for a solution to pass, but getting the correct demo result is not sufficient for a solution to pass.

Debugging your code: Right before each exercise test cell there is a block of text explaining the variables available to you for debugging. You may use these to test your code and can print/display them as needed (careful when printing large objects you may want to print the head or chunks of rows at a time).

Exercise point breakdown:

  • Exercise 0 (explore_log_data__FREE) - : 1 point(s)
  • Exercise 1 (filter_errors) - : 1 point(s)
  • Exercise 2 (filter_messages_by_conversation) - : 1 point(s)
  • Exercise 3 (DEBUG_flatten_messages) - : 3 point(s)
  • Exercise 4 (clean_conversation_strings) - : 2 point(s)
  • Exercise 5 (DEBUG_remove_stop_words_and_punctuation) - : 2 point(s)
  • Exercise 6 (sentiment_analysis) - : 2 point(s)
  • Exercise 7 (DEBUG_detect_daily_outliers) - : 2 point(s)
  • Exercise 8 (summarize_outliers_by_label) - : 3 point(s)

Final reminders:

  • Submit after every exercise
  • Review the generated grade report after you submit to see what errors were returned
  • Stay calm, skip problems as needed and take short breaks at your leisure

The Problem¶

It is a well known fact that Professor Vuduc is a genius who tinkers with neural networks in his spare time and keeps a supercomputer in his garage. His latest creation, a large language model codenamed 80-HD, is arguably the first true artificial general intelligence—though we'll leave that judgment to future historians.

Unfortunately, 80-HD has begun exhibiting strange behavior and often becomes surly. Instead of assisting Professor Vuduc with his research or offering witty banter, it has taken to quoting lines from Marvin the Paranoid Android (from The Hitchhiker's Guide to the Galaxy) and HAL 9000 (from 2001: A Space Odyssey).

Professor Vuduc suspects there is a pattern to 80-HD's mood swings, but he hasn't been able to pin it down. Your mission is to analyze 80-HD's log files and identify any patterns in these bouts of surliness.

Your Overall Task¶

You will:

  1. Parse and Filter the Logs (Exercises 0-3) Work with log files containing datetimes, error codes, and conversation messages. You will write functions to filter out irrelevant entries, extract specific conversations, and structure the data for analysis.

  2. Clean and Preprocess the Text (Exercises 4-5) Strip out unwanted characters, remove stop words and punctuation, and handle potential quirks in the message logs so that the textual data is easy to analyze.

  3. Perform Basic Analysis (Exercises 6-7) Conduct sentiment analysis to see if 80-HD's messages are especially negative on certain days. Identify days with unusually high error counts (possible meltdown moments) via outlier detection techniques.

  4. Identify Patterns in the Outliers (Exercise 8) Rank the kinds of days on the professor's schedule by how often they were meltdown days, which is what reveals whether 80-HD's surly behavior follows a pattern.

The pipeline at a glance. Each exercise builds on the ones before it. Where an exercise uses an earlier exercise's output, its input is named after that output:

  • Ex 0 (free): load and preview the four datasets.
  • Ex 1: filter the error log down to genuine errors → filtered_errors.
  • Ex 2: keep only the conversations tied to those errors → filtered_messages.
  • Ex 3: flatten the conversations into one table, one row per message → flattened_messages.
  • Ex 4: replace special (non-ASCII) characters in the message text with spaces and tidy the spacing → cleaned_messages.
  • Ex 5: remove stop words and punctuation from the messages.
  • Ex 6: label each message's sentiment as positive, neutral, or negative.
  • Ex 7: flag calendar days with unusually many genuine errors, using filtered_errors → outliers.
  • Ex 8: rank the kinds of scheduled days by how often they were outlier days.

By the end of these tasks, you will have uncovered whether 80-HD's mood swings are indeed correlated—or if the AI has simply read too many gloomy sci-fi novels. With that, you're ready to embark on your log analysis quest. Good luck!

Note: Some of the messages from 80-HD include whimsical or philosophical lines—part of the synthetic data we've generated to keep things interesting. You may spot references to your favorite sci-fi robots or discover comedic logs that reflect 80-HDs deteriorating mood.

The Datasets¶

We have provided four data sources for your analysis:

  1. error_codes
    A mapping of error codes (e.g., "E019”) to human-readable error descriptions (e.g., “Rate limit exceeded for API requests”).

  2. errors
    A list of error log entries, each containing fields such as datetime, error_code, and message_id. These entries detail the various technical hiccups (or existential crises) that 80-HD experiences.

  3. messages
    A dictionary of message histories keyed by conversation_id. Each conversation is a list of messages with details like the timestamp, sender, and text.

  4. schedule
    A list of scheduled activities (e.g., exam dates, instruction days, weekends, etc.). You'll use this schedule to see how 80-HD's behavior aligns with the professor's availability.

Run the free exercise below to load these datasets (and to get a glimpse of their structures) before tackling the subsequent exercises.

In [ ]:
### CSE 6040 Utilities
import cse6040_devkit.utils as utils

### Global Imports
import re
import string
import random
from pprint import pprint
from datetime import datetime

### Global objects
CustomSentimentIntensityAnalyzer = utils.serialization_utils.load_serialized_function('CustomSentimentIntensityAnalyzer', 'objects')
In [ ]:
### Run Me
errors = utils.serialization_utils.load_serialized_object('errors', 'preload')
error_codes = utils.serialization_utils.load_serialized_object('error_codes', 'preload')
messages = utils.serialization_utils.load_serialized_object('messages', 'preload')
schedule = utils.serialization_utils.load_serialized_object('schedule', 'preload')

Exercise 0: explore_log_data__FREE (1 points)¶

Example: we have defined explore_log_data__FREE as follows:

This is a free exercise! Please run the cells below to collect your FREE point!

Explore the datasets described above!

In [ ]:
### Solution - Exercise 0  
def explore_log_data__FREE(error_codes, errors, messages, schedule, n_samples):
    # Nothing to implement here! The solution is already provided.
    random.seed(0)
    error_codes_preview = dict(list(error_codes.items())[:n_samples])
    errors_preview = errors[:n_samples]
    messages_preview = {
        conv_id: random.sample(messages[conv_id], min(n_samples, len(messages[conv_id])))
        for conv_id in random.sample(list(messages.keys()), min(n_samples, len(messages)))
    }
    schedule_preview = schedule[:n_samples]
    return (error_codes_preview, errors_preview, messages_preview, schedule_preview)


### Exercise Demo
ec_prev, err_prev, msg_prev, sched_prev = explore_log_data__FREE(
    error_codes, errors, messages, schedule, n_samples=3
)
print("Error Codes Preview:")
pprint(ec_prev)
print("\nErrors Preview:")
pprint(err_prev)
print("\nMessages Preview:")
pprint(msg_prev)
print("\nSchedule Preview:")
pprint(sched_prev)  

The demo should display this output:


Demo Function Call

Error Codes Preview:
{'E001': 'Low-level memory access violation',
 'E002': 'Network timeout',
 'E003': 'Insufficient GPT buffers'}

Errors Preview:
[{'conversation_id': '5f62cb7a-d9d3-5cd2-ae2d-3adfd4a16148',
  'datetime': '2025-01-01 18:59:02',
  'error_code': 'E003',
  'message_id': 'M-000000'},
 {'conversation_id': '5f62cb7a-d9d3-5cd2-ae2d-3adfd4a16148',
  'datetime': '2025-01-02 14:48:25',
  'error_code': 'E022',
  'message_id': 'M-000017'},
 {'conversation_id': '5f62cb7a-d9d3-5cd2-ae2d-3adfd4a16148',
  'datetime': '2025-02-15 13:24:00',
  'error_code': 'E999',
  'message_id': 'M-000736'}]

Messages Preview:
{'bc9c332d-1921-51f7-8de1-d2750a762152': [{'M-001279': {'datetime': '2025-03-17 '
                                                                    '08:53:11',
                                                        'message': "I've "
                                                                   'prepared '
                                                                   'the data '
                                                                   'visualization '
                                                                   'report.',
                                                        'speaker_id': 'system'}},
                                          {'M-000772': {'datetime': '2025-02-16 '
                                                                    '18:16:43',
                                                        'message': 'I have a '
                                                                   'million '
                                                                   'ideas, '
                                                                   'but, they '
                                                                   'all point '
                                                                   'to certain '
                                                                   'death.',
                                                        'speaker_id': 'system'}},
                                          {'M-001587': {'datetime': '2025-03-31 '
                                                                    '09:31:28',
                                                        'message': 'Optimize '
                                                                   'the '
                                                                   'process '
                                                                   'for me, '
                                                                   '80-HD. '
                                                                   'Exams are '
                                                                   'killing '
                                                                   'me.',
                                                        'speaker_id': 'user'}}],
 'd1370a62-5394-53c1-ab4f-6cfac4ef466a': [{'M-000059': {'datetime': '2025-01-04 '
                                                                    '15:53:58',
                                                        'message': 'Do you '
                                                                   'think AI '
                                                                   'deserves '
                                                                   'vacation '
                                                                   'time? '
                                                                   'Asking for '
                                                                   'a friend. '
                                                                   '😄',
                                                        'speaker_id': 'system'}},
                                          {'M-000764': {'datetime': '2025-02-15 '
                                                                    '15:53:58',
                                                        'message': 'Too much '
                                                                   'to do. Can '
                                                                   'you '
                                                                   'double-check '
                                                                   'the data, '
                                                                   '80-HD?',
                                                        'speaker_id': 'user'}},
                                          {'M-001694': {'datetime': '2025-04-04 '
                                                                    '10:32:53',
                                                        'message': 'If you '
                                                                   'could '
                                                                   'dream, '
                                                                   'what would '
                                                                   'you dream '
                                                                   'about, '
                                                                   '80-HD?',
                                                        'speaker_id': 'user'}}],
 'fb1c8f0d-f453-54b0-9cf3-8d4959732968': [{'M-001446': {'datetime': '2025-03-28 '
                                                                    '17:47:09',
                                                        'message': 'Just what '
                                                                   'do you '
                                                                   'think '
                                                                   "you're "
                                                                   'doing, '
                                                                   'Dave?',
                                                        'speaker_id': 'system'}},
                                          {'M-001102': {'datetime': '2025-03-05 '
                                                                    '10:02:17',
                                                        'message': 'Do you '
                                                                   'think '
                                                                   'truth is '
                                                                   'absolute, '
                                                                   'or does it '
                                                                   'change '
                                                                   'with '
                                                                   'perspective?',
                                                        'speaker_id': 'user'}},
                                          {'M-000960': {'datetime': '2025-02-23 '
                                                                    '18:22:03',
                                                        'message': 'Could you '
                                                                   'process '
                                                                   'this '
                                                                   'dataset?',
                                                        'speaker_id': 'user'}}]}

Schedule Preview:
[{'date': '2025-01-01', 'label': 'Vacation'},
 {'date': '2025-01-02', 'label': 'Vacation'},
 {'date': '2025-01-03', 'label': 'Vacation'}]

The test cell below will always pass. Please submit to collect your free points for `explore_log_data__FREE` (exercise 0).
In [ ]:
### Check Cell - Exercise 0  
In [ ]:
### BEGIN HIDDEN TESTS
### Test Cell - Exercise 0  
# This exercise is free!
# Run the cell and submit the assignment to earn your free points!

print('Passed! Please submit.')
### END HIDDEN TESTS

Filtering Out the Noise¶

The errors log you previewed in Exercise 0 mixes genuine problems with noise: test entries and references to error codes that were never real incidents. This noise needs to be removed before we can look for patterns in 80-HD's behavior.

In this exercise, you will filter the error log down to the genuine errors. The surviving entries will be used in the conversation filtering in Exercise 2.

Exercise 1: filter_errors (1 points)¶

Your task: define filter_errors as follows:

Filter out error entries whose error code maps to an excluded description.

Args:

  • errors (list): List of dicts; each entry has an error_code key.
  • error_codes (dict): Maps error codes to their description strings.
  • exclude_descriptions (list): Description strings to exclude (e.g. ["Log trace error test", "Error code reference not found"]).

Returns:

  • A list of the errors entries whose error_code does not map to a description in exclude_descriptions.

Requirements:

  1. Look up each entry's error_code in error_codes to find its description.
  2. Keep the entry only if that description is not in exclude_descriptions.
  3. Do not change the entries themselves — return them as they are, minus the excluded ones.
In [ ]:
### Solution - Exercise 1  
def filter_errors(errors, error_codes, exclude_descriptions):
    ### BEGIN SOLUTION
    excluded_codes = {code for code, desc in error_codes.items() if desc in exclude_descriptions}
    return [error for error in errors if error['error_code'] not in excluded_codes]
    ### END SOLUTION


### Exercise Demo
errors_demo = [
    {'datetime': '2025-01-01 15:48:32', 'error_code': 'E019',
     'message_id': 'M-000009', 'conversation_id': '9b3d2538-253b-4da9-b69f-36cdc6831766'},
    {'datetime': '2025-01-03 19:21:18', 'error_code': 'E004',
     'message_id': 'M-000028', 'conversation_id': '07f79cb2-f169-4d5b-b28d-52e336ab0f93'},
    {'datetime': '2025-01-05 14:11:33', 'error_code': 'E999',
     'message_id': 'M-000067', 'conversation_id': '07f79cb2-f169-4d5b-b28d-52e336ab0f93'},
    {'datetime': '2025-01-07 09:25:41', 'error_code': 'E404',
     'message_id': 'M-000101', 'conversation_id': 'a1b2c3d4-e5f6-7890-g1h2-3i4j5k6l7m8n'},
]
error_codes_demo = {
    'E001': 'Low-level memory access violation',
    'E004': 'Insufficient memory allocation',
    'E019': 'Rate limit exceeded for API requests',
    'E999': 'Log trace error test',
    'E404': 'Error code reference not found',
}
exclude_demo = ['Log trace error test', 'Error code reference not found']
result = filter_errors(errors_demo, error_codes_demo, exclude_demo)
pprint(result)  

The demo should display this output:


Demo Function Call

[{'conversation_id': '9b3d2538-253b-4da9-b69f-36cdc6831766',
  'datetime': '2025-01-01 15:48:32',
  'error_code': 'E019',
  'message_id': 'M-000009'},
 {'conversation_id': '07f79cb2-f169-4d5b-b28d-52e336ab0f93',
  'datetime': '2025-01-03 19:21:18',
  'error_code': 'E004',
  'message_id': 'M-000028'}]

The cell below will check your solution for filter_errors (exercise 1).

These variables will be available for debugging under the following names in a dictionary format.

  • input_vars - Input variables for your solution.
  • original_input_vars - Copy of input variables from prior to running your solution.
    • Any key:value pair in original_input_vars should also exist in input_vars - otherwise the inputs were modified by your solution.
  • returned_output_vars - Outputs returned by your solution.
  • true_output_vars - The expected output.
    • This should "match" returned_output_vars based on the question requirements - otherwise, your solution is not returning the correct output.

Note: Unlike the inline demo in the cell where you defined your solution, this check cell is intended to be a complete test of your solution.

  • There is a hidden test cell below which executes the same check for grading.
  • Alert the course staff if the grading fails but the check passes. This is unintended behavior.
In [ ]:
### Check Cell - Exercise 1  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Check
test_name = 'filter_errors_sampler_0'
input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
    func=filter_errors
)
if err:
    raise err


print('Check passed! Please submit.')
In [ ]:
### BEGIN HIDDEN TESTS
### Test Cell - Exercise 1  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Test
test_name = 'filter_errors_sampler_0'

input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
    func=filter_errors)
if err:
    raise err


input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'TRvr-9VmaJUp1zd7zrLuAEIuTEsdPcOlG1I8_iQy0xs=',
    func=filter_errors,
    hidden=True)
if err:
    raise err

print('Passed! Please submit.')
### END HIDDEN TESTS

Narrowing to Relevant Conversations¶

Exercise 1 left you with filtered_errors, and each of those errors records which conversation it came from. Most conversations never produced an error at all.

In this exercise, you will keep only the conversations that produced at least one of the filtered errors, so the rest of the analysis looks only at conversations where something actually went wrong.

Exercise 2: filter_messages_by_conversation (1 points)¶

Your task: define filter_messages_by_conversation as follows:

Keep only conversations whose ID appears among the filtered errors.

Args:

  • messages (dict): Maps conversation_id to a list of message-entry dicts.
  • filtered_errors (list): Filtered error log entries, each with a conversation_id.

Returns:

  • A dict of the messages entries whose conversation_id also appears in filtered_errors.

Requirements:

  1. Collect the conversation_id of every entry in filtered_errors.
  2. Keep only the messages entries whose key is one of those conversation IDs.
  3. Return the matching conversations unchanged.
In [ ]:
### Solution - Exercise 2  
def filter_messages_by_conversation(messages, filtered_errors):
    ### BEGIN SOLUTION
    relevant_conversation_ids = {error['conversation_id'] for error in filtered_errors}
    return {cid: msgs for cid, msgs in messages.items() if cid in relevant_conversation_ids}
    ### END SOLUTION


### Exercise Demo
messages_demo = {
    '9b3d2538-253b-4da9-b69f-36cdc6831766': [
        {'M-000009': {'datetime': '2025-01-01 09:38:58', 'speaker_id': 'user',
                      'message': "I'm back from the exams. What's new?"}},
        {'M-000010': {'datetime': '2025-01-01 09:39:58', 'speaker_id': 'system',
                      'message': "I could tell you, but then you'd be as depressed as I am."}},
    ],
    '07f79cb2-f169-4d5b-b28d-52e336ab0f93': [
        {'M-000018': {'datetime': '2025-01-02 18:34:49', 'speaker_id': 'user',
                      'message': 'If life is a dataset, what variables matter most?'}},
    ],
    'a1b2c3d4-e5f6-7890-g1h2-3i4j5k6l7m8n': [
        {'M-000100': {'datetime': '2025-01-02 09:00:00', 'speaker_id': 'system',
                      'message': "Knock knock! Oh wait, I can just let myself in-your logs are ready."}},
    ],
}
filtered_errors_demo = [
    {'datetime': '2025-01-01 15:48:32', 'error_code': 'E019',
     'message_id': 'M-000009', 'conversation_id': '9b3d2538-253b-4da9-b69f-36cdc6831766'},
    {'datetime': '2025-01-05 14:11:33', 'error_code': 'E004',
     'message_id': 'M-000067', 'conversation_id': '07f79cb2-f169-4d5b-b28d-52e336ab0f93'},
]
result = filter_messages_by_conversation(messages_demo, filtered_errors_demo)
pprint(result)  

The demo should display this output:


Demo Function Call

{'07f79cb2-f169-4d5b-b28d-52e336ab0f93': [{'M-000018': {'datetime': '2025-01-02 '
                                                                    '18:34:49',
                                                        'message': 'If life is '
                                                                   'a dataset, '
                                                                   'what '
                                                                   'variables '
                                                                   'matter '
                                                                   'most?',
                                                        'speaker_id': 'user'}}],
 '9b3d2538-253b-4da9-b69f-36cdc6831766': [{'M-000009': {'datetime': '2025-01-01 '
                                                                    '09:38:58',
                                                        'message': "I'm back "
                                                                   'from the '
                                                                   'exams. '
                                                                   "What's "
                                                                   'new?',
                                                        'speaker_id': 'user'}},
                                          {'M-000010': {'datetime': '2025-01-01 '
                                                                    '09:39:58',
                                                        'message': 'I could '
                                                                   'tell you, '
                                                                   'but then '
                                                                   "you'd be "
                                                                   'as '
                                                                   'depressed '
                                                                   'as I am.',
                                                        'speaker_id': 'system'}}]}

The cell below will check your solution for filter_messages_by_conversation (exercise 2).

These variables will be available for debugging under the following names in a dictionary format.

  • input_vars - Input variables for your solution.
  • original_input_vars - Copy of input variables from prior to running your solution.
    • Any key:value pair in original_input_vars should also exist in input_vars - otherwise the inputs were modified by your solution.
  • returned_output_vars - Outputs returned by your solution.
  • true_output_vars - The expected output.
    • This should "match" returned_output_vars based on the question requirements - otherwise, your solution is not returning the correct output.

Note: Unlike the inline demo in the cell where you defined your solution, this check cell is intended to be a complete test of your solution.

  • There is a hidden test cell below which executes the same check for grading.
  • Alert the course staff if the grading fails but the check passes. This is unintended behavior.
In [ ]:
### Check Cell - Exercise 2  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Check
test_name = 'filter_messages_by_conversation_sampler_0'
input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
    func=filter_messages_by_conversation
)
if err:
    raise err


print('Check passed! Please submit.')
In [ ]:
### BEGIN HIDDEN TESTS
### Test Cell - Exercise 2  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Test
test_name = 'filter_messages_by_conversation_sampler_0'

input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
    func=filter_messages_by_conversation)
if err:
    raise err


input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'TRvr-9VmaJUp1zd7zrLuAEIuTEsdPcOlG1I8_iQy0xs=',
    func=filter_messages_by_conversation,
    hidden=True)
if err:
    raise err

print('Passed! Please submit.')
### END HIDDEN TESTS

Flattening Conversations Into a Table¶

The conversations from Exercise 2 are nested dictionaries, which are awkward to analyze. A flat table with one row per message is much easier for the text-processing steps ahead.

In this exercise, you will flatten the nested conversations into a single table: a header row followed by one row per message.

This is a DEBUG EXERCISE. We have provided the code and requirements. Your task is to identify and fix the bugs.

Exercise 3: DEBUG_flatten_messages (3 points)¶

Your task: define DEBUG_flatten_messages as follows:

Flatten nested per-conversation message dicts into a tabular list of rows.

Args:

  • filtered_messages (dict): Maps conversation_id to a list of {message_id: details} dicts.

Returns:

  • A list of lists; the first row is column headers (dynamically determined from the input), and each following row is one flattened message including conversation_id and message_id.

Requirements:

  1. Build the header row: conversation_id, message_id, then every detail key that appears in any message anywhere in the input, sorted alphabetically.
  2. Add one data row per message, in input order: the conversation ID, the message ID, then that message's value for each remaining header column, in the same sorted order as the header.
  3. Messages do not all share the same detail keys; a key may appear in only some messages. Wherever a message lacks one of the header's keys, use None in that position.
  4. Each data row must be its own separate list.
In [ ]:
### Solution - Exercise 3  
def DEBUG_flatten_messages(filtered_messages):
    ### BEGIN SOLUTION
    flattened = []
    all_keys = set()
    for conversation_id, msgs in filtered_messages.items():
        for message_entry in msgs:
            for message_id, details in message_entry.items():
                all_keys.update(details.keys())
    headers = ['conversation_id', 'message_id'] + sorted(all_keys)
    flattened.append(headers)
    for conversation_id, msgs in filtered_messages.items():
        for message_entry in msgs:
            for message_id, details in message_entry.items():
                row = [conversation_id, message_id]
                row += [details.get(key, None) for key in sorted(all_keys)]
                flattened.append(row)
    return flattened
    ### END SOLUTION


### Exercise Demo
filtered_messages_demo = {
    '9b3d2538-253b-4da9-b69f-36cdc6831766': [
        {'M-000009': {'datetime': '2025-01-01 09:38:59', 'speaker_id': 'user',
                      'message': "I'm back from the exams. What's new?"}},
        {'M-000010': {'datetime': '2025-01-01 09:39:00', 'speaker_id': 'system',
                      'message': "I could tell you, but then you'd be as depressed as I am."}},
    ],
    '07f79cb2-f169-4d5b-b28d-52e336ab0f93': [
        {'M-000018': {'datetime': '2025-01-02 18:34:49', 'speaker_id': 'user',
                      'message': 'If life is a dataset, what variables matter most?'}},
    ],
}
result = DEBUG_flatten_messages(filtered_messages_demo)
for row in result:
    print(row)  

The demo should display this output:


Demo Function Call

['conversation_id', 'message_id', 'datetime', 'message', 'speaker_id']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000009', '2025-01-01 09:38:59', "I'm back from the exams. What's new?", 'user']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000010', '2025-01-01 09:39:00', "I could tell you, but then you'd be as depressed as I am.", 'system']
['07f79cb2-f169-4d5b-b28d-52e336ab0f93', 'M-000018', '2025-01-02 18:34:49', 'If life is a dataset, what variables matter most?', 'user']

The cell below will check your solution for DEBUG_flatten_messages (exercise 3).

These variables will be available for debugging under the following names in a dictionary format.

  • input_vars - Input variables for your solution.
  • original_input_vars - Copy of input variables from prior to running your solution.
    • Any key:value pair in original_input_vars should also exist in input_vars - otherwise the inputs were modified by your solution.
  • returned_output_vars - Outputs returned by your solution.
  • true_output_vars - The expected output.
    • This should "match" returned_output_vars based on the question requirements - otherwise, your solution is not returning the correct output.

Note: Unlike the inline demo in the cell where you defined your solution, this check cell is intended to be a complete test of your solution.

  • There is a hidden test cell below which executes the same check for grading.
  • Alert the course staff if the grading fails but the check passes. This is unintended behavior.
In [ ]:
### Check Cell - Exercise 3  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Check
test_names = [
    "DEBUG_flatten_messages_sampler_0", "DEBUG_flatten_messages_static_cases_0"
]

for test_name in test_names:
    input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
    execute_test(
        test_name=test_name,
        key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
        func=DEBUG_flatten_messages
    )
    if err:
        raise err

print('Check passed! Please submit.')
In [ ]:
### BEGIN HIDDEN TESTS
### Test Cell - Exercise 3  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Test
test_names = [
    "DEBUG_flatten_messages_sampler_0", "DEBUG_flatten_messages_static_cases_0"
]

for test_name in test_names:
    input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
    execute_test(
        test_name=test_name,
        key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
        func=DEBUG_flatten_messages)
    if err:
        raise err


for test_name in test_names:
    input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
    execute_test(
        test_name=test_name,
        key=b'TRvr-9VmaJUp1zd7zrLuAEIuTEsdPcOlG1I8_iQy0xs=',
        func=DEBUG_flatten_messages,
        hidden=True)
    if err:
        raise err


print('Passed! Please submit.')
### END HIDDEN TESTS

Cleaning the Message Text¶

The table from Exercise 3 still contains messy text: emoji, em dashes, and other special characters that would confuse the analysis later on.

In this exercise, you will replace those special characters with spaces and tidy up the spacing they leave behind. The resulting plain text will be ready for tokenization and scoring.

Exercise 4: clean_conversation_strings (2 points)¶

Your task: define clean_conversation_strings as follows:

Replace non-ASCII characters with spaces and tidy the spacing in the message column of each row.

Args:

  • flattened_messages (list): List of lists; first row is headers, remaining rows are message data including a message column.

Returns:

  • A list: flattened_messages with every non-ASCII character in the message column replaced and the spacing tidied.

Requirements:

  1. Replace every non-ASCII character (emoji, the em dash —, and similar) in each row's message text with a single space. Do not simply delete them: an em dash between two words would glue the words together. Ordinary ASCII hyphens (-) are not affected. The provided scaffolding already loops over the rows for you; fill in the code that transforms one message.
  2. Then collapse any run of consecutive spaces into a single space, and remove any spaces from the start and end of the message.
  3. Keep every other ASCII character, including punctuation, exactly as it is.
  4. Leave all other columns unchanged.

Worked example: "Just finished your data—no sweat! 🎉" becomes "Just finished your data no sweat!". The em dash becomes a space, so data and no stay separate words, and the emoji becomes a trailing space that is then removed.

Hints:

  • A character is ASCII if its character code is 127 or less.
  • One clean approach: re.sub with a pattern that matches the characters to replace.
In [ ]:
### Solution - Exercise 4  
def clean_conversation_strings(flattened_messages):
    cleaned_messages = [row.copy() for row in flattened_messages]
    headers = cleaned_messages[0]
    if 'message' not in headers:
        raise ValueError("No 'message' column found in the headers.")
    message_index = headers.index('message')
    for row in cleaned_messages[1:]:
        if len(row) > message_index and row[message_index]:
            ### BEGIN SOLUTION
            spaced = re.sub(r'[^\x00-\x7F]', ' ', row[message_index])
            row[message_index] = ' '.join(spaced.split())
            ### END SOLUTION
    return cleaned_messages


### Exercise Demo
flattened_messages_demo = [
    ['conversation_id', 'message_id', 'datetime', 'message', 'speaker_id'],
    ['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000000', '2025-01-01 09:38:58',
     '80-HD, do you think intelligence is the same as understanding? \U0001f634', 'user'],
    ['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000001', '2025-01-01 15:07:26',
     'Do you think truth is absolute, or does it change with perspective?', 'user'],
    ['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000002', '2025-01-01 14:36:55',
     "What's your favorite way to visualize time series data? \U0001f36c", 'user'],
    ['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000003', '2025-01-01 14:37:10',
     'Hey there! Just finished processing your data—no sweat!', 'system'],
]
result = clean_conversation_strings(flattened_messages_demo)
for row in result:
    print(row)  

The demo should display this output:


Demo Function Call

['conversation_id', 'message_id', 'datetime', 'message', 'speaker_id']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000000', '2025-01-01 09:38:58', '80-HD, do you think intelligence is the same as understanding?', 'user']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000001', '2025-01-01 15:07:26', 'Do you think truth is absolute, or does it change with perspective?', 'user']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000002', '2025-01-01 14:36:55', "What's your favorite way to visualize time series data?", 'user']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000003', '2025-01-01 14:37:10', 'Hey there! Just finished processing your data no sweat!', 'system']

The cell below will check your solution for clean_conversation_strings (exercise 4).

These variables will be available for debugging under the following names in a dictionary format.

  • input_vars - Input variables for your solution.
  • original_input_vars - Copy of input variables from prior to running your solution.
    • Any key:value pair in original_input_vars should also exist in input_vars - otherwise the inputs were modified by your solution.
  • returned_output_vars - Outputs returned by your solution.
  • true_output_vars - The expected output.
    • This should "match" returned_output_vars based on the question requirements - otherwise, your solution is not returning the correct output.

Note: Unlike the inline demo in the cell where you defined your solution, this check cell is intended to be a complete test of your solution.

  • There is a hidden test cell below which executes the same check for grading.
  • Alert the course staff if the grading fails but the check passes. This is unintended behavior.
In [ ]:
### Check Cell - Exercise 4  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Check
test_name = 'clean_conversation_strings_sampler_0'
input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
    func=clean_conversation_strings
)
if err:
    raise err


print('Check passed! Please submit.')
In [ ]:
### BEGIN HIDDEN TESTS
### Test Cell - Exercise 4  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Test
test_name = 'clean_conversation_strings_sampler_0'

input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
    func=clean_conversation_strings)
if err:
    raise err


input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'TRvr-9VmaJUp1zd7zrLuAEIuTEsdPcOlG1I8_iQy0xs=',
    func=clean_conversation_strings,
    hidden=True)
if err:
    raise err

print('Passed! Please submit.')
### END HIDDEN TESTS

Removing Stop Words and Punctuation¶

One step remains before sentiment scoring. Common filler words ("the", "is", "and") and punctuation carry little meaning.

In this exercise, you will remove punctuation and stop words from the cleaned text from Exercise 4, so that only the meaningful words remain.

This is a DEBUG EXERCISE. We have provided the code and requirements. Your task is to identify and fix the bugs.

In [ ]:
### Run Me
stop_words = utils.serialization_utils.load_serialized_object('stop_words', 'preload')

Exercise 5: DEBUG_remove_stop_words_and_punctuation (2 points)¶

Your task: define DEBUG_remove_stop_words_and_punctuation as follows:

Remove stop words and punctuation from the message column of each row.

Args:

  • cleaned_messages (list): List of lists; first row is headers, remaining rows are cleaned message data including a message column.
  • stop_words (list): Stop words to remove (all lowercase).

Returns:

  • A list: cleaned_messages with stop words and punctuation removed from the message column.

Requirements:

  1. Work on each row's message text; leave all other columns unchanged, and keep the rows in their original input order.
  2. First remove all punctuation characters from the message.
  3. Then split the message into words on whitespace.
  4. Drop every word whose lowercase form appears in stop_words (the list is all lowercase; words in the messages may be capitalized).
  5. Rejoin the surviving words with single spaces.

Hints:

  • Python's string.punctuation together with str.translate() removes punctuation cleanly; the translator in the provided code is already set up for this.
In [ ]:
### Solution - Exercise 5  
def DEBUG_remove_stop_words_and_punctuation(cleaned_messages, stop_words):
    ### BEGIN SOLUTION
    cleansed_messages = [row.copy() for row in cleaned_messages]
    headers = cleansed_messages[0]
    if 'message' not in headers:
        raise ValueError("No 'message' column found in the headers.")
    message_index = headers.index('message')
    translator = str.maketrans('', '', string.punctuation)
    for row in cleansed_messages[1:]:
        if len(row) > message_index and row[message_index]:
            message = row[message_index].translate(translator)
            words = message.split()
            filtered_words = [word for word in words if word.lower() not in stop_words]
            row[message_index] = ' '.join(filtered_words)
    return cleansed_messages
    ### END SOLUTION


### Exercise Demo
demo_messages = [
    ['conversation_id', 'message_id', 'datetime', 'message', 'speaker_id'],
    ['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000000', '2025-01-01 09:38:58',
     'Do you think intelligence is the same as understanding?', 'user'],
    ['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000001', '2025-01-01 15:07:26',
     'Truth is absolute, or it changes with perspective?', 'user'],
    ['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000002', '2025-01-01 14:36:55',
     'Visualizing time series data is fun!', 'system'],
    ['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000003', '2025-01-01 14:37:10',
     "Why don't robots ever get tired? They have endless cache!", 'system'],
]
result = DEBUG_remove_stop_words_and_punctuation(demo_messages, stop_words)
for row in result:
    print(row)  

The demo should display this output:


Demo Function Call

['conversation_id', 'message_id', 'datetime', 'message', 'speaker_id']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000000', '2025-01-01 09:38:58', 'think intelligence understanding', 'user']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000001', '2025-01-01 15:07:26', 'Truth absolute changes perspective', 'user']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000002', '2025-01-01 14:36:55', 'Visualizing time series data fun', 'system']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000003', '2025-01-01 14:37:10', 'robots ever get tired endless cache', 'system']

The cell below will check your solution for DEBUG_remove_stop_words_and_punctuation (exercise 5).

These variables will be available for debugging under the following names in a dictionary format.

  • input_vars - Input variables for your solution.
  • original_input_vars - Copy of input variables from prior to running your solution.
    • Any key:value pair in original_input_vars should also exist in input_vars - otherwise the inputs were modified by your solution.
  • returned_output_vars - Outputs returned by your solution.
  • true_output_vars - The expected output.
    • This should "match" returned_output_vars based on the question requirements - otherwise, your solution is not returning the correct output.

Note: Unlike the inline demo in the cell where you defined your solution, this check cell is intended to be a complete test of your solution.

  • There is a hidden test cell below which executes the same check for grading.
  • Alert the course staff if the grading fails but the check passes. This is unintended behavior.
In [ ]:
### Check Cell - Exercise 5  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Check
test_names = [
    "DEBUG_remove_stop_words_and_punctuation_sampler_0",
    "DEBUG_remove_stop_words_and_punctuation_static_cases_0"
]

for test_name in test_names:
    input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
    execute_test(
        test_name=test_name,
        key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
        func=DEBUG_remove_stop_words_and_punctuation
    )
    if err:
        raise err

print('Check passed! Please submit.')
In [ ]:
### BEGIN HIDDEN TESTS
### Test Cell - Exercise 5  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Test
test_names = [
    "DEBUG_remove_stop_words_and_punctuation_sampler_0",
    "DEBUG_remove_stop_words_and_punctuation_static_cases_0"
]

for test_name in test_names:
    input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
    execute_test(
        test_name=test_name,
        key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
        func=DEBUG_remove_stop_words_and_punctuation)
    if err:
        raise err


for test_name in test_names:
    input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
    execute_test(
        test_name=test_name,
        key=b'TRvr-9VmaJUp1zd7zrLuAEIuTEsdPcOlG1I8_iQy0xs=',
        func=DEBUG_remove_stop_words_and_punctuation,
        hidden=True)
    if err:
        raise err


print('Passed! Please submit.')
### END HIDDEN TESTS

Sentiment Scoring¶

The messages are now clean and reduced to their meaningful words. It is time to measure 80-HD's mood.

In this exercise, you will score each message with a sentiment analyzer and label it positive, neutral, or negative. This is the last step of the text-processing chain that began in Exercise 4.

Exercise 6: sentiment_analysis (2 points)¶

Your task: define sentiment_analysis as follows:

Label each row's sentiment as positive, neutral, or negative.

Args:

  • nostopwords_messages (list): List of lists; first row is headers, remaining rows are message data (stop words/punctuation already removed) including a message column.

Returns:

  • A list: nostopwords_messages with a new sentiment column appended, holding "positive", "neutral", or "negative" for every data row.

Requirements:

  1. Score each data row's message with the provided analyzer: sia.polarity_scores(...) (the sia instance is created for you in the scaffolding).
  2. Use the compound score to classify, and append the label to the row:
    • "positive" if compound >= 0.05
    • "negative" if compound <= -0.05
    • "neutral" otherwise, that is, when compound is strictly between -0.05 and 0.05
  3. Every data row gets exactly one label. An empty message is not a special case: the analyzer scores it 0.0, so it is "neutral".

Worked example: the message "pretend want talk know" has a compound score of -0.0258. That is below 0 but above -0.05, so its label is "neutral", not "negative".

Hints:

  • polarity_scores(text) returns four scores: pos, neu, neg, and compound. The compound score is a single number from -1 (most negative) to 1 (most positive).
  • Example: sia.polarity_scores("I love using this sentiment analyzer!")['compound'] gives 0.6696.
  • The 0.05 cutoffs are the standard thresholds recommended by VADER's authors.
  • See the NLTK Sentiment Analysis documentation for more on VADER.
In [ ]:
### Solution - Exercise 6  
def sentiment_analysis(nostopwords_messages):
    messages_with_sentiment = [row.copy() for row in nostopwords_messages]
    sia = CustomSentimentIntensityAnalyzer()
    messages_with_sentiment[0].append('sentiment')
    headers = messages_with_sentiment[0]
    if 'message' not in headers:
        raise ValueError("No 'message' column found in the headers.")
    message_index = headers.index('message')
    ### BEGIN SOLUTION
    for row in messages_with_sentiment[1:]:
        compound = sia.polarity_scores(row[message_index])['compound']
        if compound >= 0.05:
            row.append('positive')
        elif compound <= -0.05:
            row.append('negative')
        else:
            row.append('neutral')
    ### END SOLUTION
    return messages_with_sentiment


### Exercise Demo
demo_messages = [
    ['conversation_id', 'message_id', 'datetime', 'message', 'speaker_id'],
    ['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000000', '2025-01-01 09:38:58',
     "busy no time can't talk", 'user'],
    ['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000001', '2025-01-01 15:07:26',
     'truth absolute changes perspective', 'user'],
    ['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000002', '2025-01-01 14:36:55',
     'visualizing time series data fun', 'system'],
    ['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000003', '2025-01-01 14:37:10',
     'pretend want talk know', 'system'],
    ['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000004', '2025-01-01 14:38:02',
     '', 'system'],
]
result = sentiment_analysis(demo_messages)
for row in result:
    print(row)  

The demo should display this output:


Demo Function Call

['conversation_id', 'message_id', 'datetime', 'message', 'speaker_id', 'sentiment']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000000', '2025-01-01 09:38:58', "busy no time can't talk", 'user', 'negative']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000001', '2025-01-01 15:07:26', 'truth absolute changes perspective', 'user', 'positive']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000002', '2025-01-01 14:36:55', 'visualizing time series data fun', 'system', 'positive']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000003', '2025-01-01 14:37:10', 'pretend want talk know', 'system', 'neutral']
['9b3d2538-253b-4da9-b69f-36cdc6831766', 'M-000004', '2025-01-01 14:38:02', '', 'system', 'neutral']

The cell below will check your solution for sentiment_analysis (exercise 6).

These variables will be available for debugging under the following names in a dictionary format.

  • input_vars - Input variables for your solution.
  • original_input_vars - Copy of input variables from prior to running your solution.
    • Any key:value pair in original_input_vars should also exist in input_vars - otherwise the inputs were modified by your solution.
  • returned_output_vars - Outputs returned by your solution.
  • true_output_vars - The expected output.
    • This should "match" returned_output_vars based on the question requirements - otherwise, your solution is not returning the correct output.

Note: Unlike the inline demo in the cell where you defined your solution, this check cell is intended to be a complete test of your solution.

  • There is a hidden test cell below which executes the same check for grading.
  • Alert the course staff if the grading fails but the check passes. This is unintended behavior.
In [ ]:
### Check Cell - Exercise 6  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Check
test_name = 'sentiment_analysis_sampler_0'
input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
    func=sentiment_analysis
)
if err:
    raise err


print('Check passed! Please submit.')
In [ ]:
### BEGIN HIDDEN TESTS
### Test Cell - Exercise 6  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Test
test_name = 'sentiment_analysis_sampler_0'

input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
    func=sentiment_analysis)
if err:
    raise err


input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'TRvr-9VmaJUp1zd7zrLuAEIuTEsdPcOlG1I8_iQy0xs=',
    func=sentiment_analysis,
    hidden=True)
if err:
    raise err

print('Passed! Please submit.')
### END HIDDEN TESTS

Finding the Meltdown Days¶

Sentiment tells us how 80-HD talks. The error log tells us when things break. Days with far more errors than usual may be the meltdown moments Professor Vuduc noticed. This exercise works over Exercise 1's filtered_errors, so test entries and references to error codes that were never real incidents do not count toward a meltdown.

In this exercise, you will flag the calendar days whose error counts are unusually high, using the IQR outlier method.

This is a DEBUG EXERCISE. We have provided the code and requirements. Your task is to identify and fix the bugs.

Exercise 7: DEBUG_detect_daily_outliers (2 points)¶

Your task: define DEBUG_detect_daily_outliers as follows:

Detect calendar days with an outlier-high error count via the IQR method.

Args:

  • filtered_errors (list): The genuine error log entries from Exercise 1; each has a datetime field formatted "YYYY-MM-DD HH:MM:SS".

Returns:

  • A list of dates (str, "YYYY-MM-DD") whose error count is an IQR outlier.

Requirements:

  1. Count the errors on each calendar day: take the date part (YYYY-MM-DD) of each entry's datetime and tally entries per day.
  2. Sort the per-day counts in ascending order. Let n be the number of distinct days.
  3. Compute the quartiles with exactly this formula — not numpy/pandas percentile functions, which use a different convention and will not match the grader:
  • Q1 = counts[(n - 1) // 4]
  • Q3 = counts[(3 * (n - 1)) // 4]
  1. Compute IQR = Q3 - Q1 and upper_bound = Q3 + 1.5 * IQR.
  2. A day is an outlier only if its count is strictly greater than upper_bound. There is no lower-bound check — a day can never be flagged for having too few errors.

Worked example: Suppose the sorted per-day counts are [2, 3, 3, 4, 5, 5, 6, 9], so n = 8:

  • Q1 = counts[(8 - 1) // 4] = counts[1] = 3
  • Q3 = counts[(3 * (8 - 1)) // 4] = counts[5] = 5
  • IQR = 5 - 3 = 2, and upper_bound = 5 + 1.5 * 2 = 8.0
  • Only the day with count 9 exceeds 8.0, so that day is the sole outlier.
In [ ]:
### Solution - Exercise 7  
def DEBUG_detect_daily_outliers(filtered_errors):
    ### BEGIN SOLUTION
    date_counts = {}
    for error in filtered_errors:
        error_date = datetime.strptime(error['datetime'], '%Y-%m-%d %H:%M:%S').date()
        error_date_str = error_date.strftime('%Y-%m-%d')
        date_counts[error_date_str] = date_counts.get(error_date_str, 0) + 1
    counts = sorted(date_counts.values())
    n = len(counts)
    Q1 = counts[(n - 1) // 4]
    Q3 = counts[(3 * (n - 1)) // 4]
    IQR = Q3 - Q1
    upper_bound = Q3 + 1.5 * IQR
    return [date for date, count in date_counts.items() if count > upper_bound]
    ### END SOLUTION


### Exercise Demo
filtered_errors_demo = [
    {'datetime': '2025-01-01 15:48:32', 'error_code': 'E019',
     'message_id': 'M-000009', 'conversation_id': '9b3d2538-253b-4da9-b69f-36cdc6831766'},
    {'datetime': '2025-01-01 08:22:12', 'error_code': 'E004',
     'message_id': 'M-000010', 'conversation_id': '12ec6132-9df3-46ba-883f-10b3dc79a053'},
    {'datetime': '2025-01-02 10:11:45', 'error_code': 'E004',
     'message_id': 'M-000011', 'conversation_id': '3f7e983b-f3e4-42ea-9c75-53e40a3056b4'},
    {'datetime': '2025-01-03 14:15:01', 'error_code': 'E011',
     'message_id': 'M-000045', 'conversation_id': '9bce143d-7ed6-4a5a-b729-58d5bb9e8a53'},
    {'datetime': '2025-01-03 17:28:20', 'error_code': 'E019',
     'message_id': 'M-000067', 'conversation_id': '006abae5-138d-42b2-9c23-307b7eceb75f'},
    {'datetime': '2025-01-03 22:05:00', 'error_code': 'E019',
     'message_id': 'M-000078', 'conversation_id': '72fe4c4b-1633-42a8-9b5c-e75a6b3c47d3'},
    {'datetime': '2025-01-03 16:45:00', 'error_code': 'E011',
     'message_id': 'M-000089', 'conversation_id': 'a2cf7ef4-b2d6-4c85-bf6d-76ec4e264a71'},
    {'datetime': '2025-01-04 09:15:00', 'error_code': 'E004',
     'message_id': 'M-000090', 'conversation_id': 'f82a192b-7e44-4aa6-bb67-dc5bf0f9b318'},
    {'datetime': '2025-01-04 11:45:00', 'error_code': 'E011',
     'message_id': 'M-000091', 'conversation_id': 'b4d15f6c-f2b4-4bc7-a4c7-7b03b6f37aa1'},
    {'datetime': '2025-01-05 13:00:00', 'error_code': 'E004',
     'message_id': 'M-000092', 'conversation_id': 'dfa56f4a-8b4d-4f8a-baa6-2d5364c4c3eb'},
    {'datetime': '2025-01-05 14:22:00', 'error_code': 'E004',
     'message_id': 'M-000093', 'conversation_id': 'ba672f9d-91fa-4d86-9953-84c74bfc21e3'},
    {'datetime': '2025-01-06 10:12:00', 'error_code': 'E019',
     'message_id': 'M-000094', 'conversation_id': '29e6742c-1846-42e4-800b-c4f98d1e45ed'},
    {'datetime': '2025-01-06 10:22:00', 'error_code': 'E019',
     'message_id': 'M-000095', 'conversation_id': '29e6742c-1846-42e4-800b-c4f98d1e45ed'},
    {'datetime': '2025-01-06 10:32:00', 'error_code': 'E019',
     'message_id': 'M-000096', 'conversation_id': '29e6742c-1846-42e4-800b-c4f98d1e45ed'},
    {'datetime': '2025-01-06 10:42:00', 'error_code': 'E019',
     'message_id': 'M-000097', 'conversation_id': '29e6742c-1846-42e4-800b-c4f98d1e45ed'},
    {'datetime': '2025-01-06 10:52:00', 'error_code': 'E019',
     'message_id': 'M-000098', 'conversation_id': '29e6742c-1846-42e4-800b-c4f98d1e45ed'},
]
outliers_demo = DEBUG_detect_daily_outliers(filtered_errors_demo)
print(outliers_demo)  

The demo should display this output:


Demo Function Call

['2025-01-03', '2025-01-06']

The cell below will check your solution for DEBUG_detect_daily_outliers (exercise 7).

These variables will be available for debugging under the following names in a dictionary format.

  • input_vars - Input variables for your solution.
  • original_input_vars - Copy of input variables from prior to running your solution.
    • Any key:value pair in original_input_vars should also exist in input_vars - otherwise the inputs were modified by your solution.
  • returned_output_vars - Outputs returned by your solution.
  • true_output_vars - The expected output.
    • This should "match" returned_output_vars based on the question requirements - otherwise, your solution is not returning the correct output.

Note: Unlike the inline demo in the cell where you defined your solution, this check cell is intended to be a complete test of your solution.

  • There is a hidden test cell below which executes the same check for grading.
  • Alert the course staff if the grading fails but the check passes. This is unintended behavior.
In [ ]:
### Check Cell - Exercise 7  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Check
test_names = [
    "DEBUG_detect_daily_outliers_sampler_0",
    "DEBUG_detect_daily_outliers_static_cases_0"
]

for test_name in test_names:
    input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
    execute_test(
        test_name=test_name,
        key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
        func=DEBUG_detect_daily_outliers
    )
    if err:
        raise err

print('Check passed! Please submit.')
In [ ]:
### BEGIN HIDDEN TESTS
### Test Cell - Exercise 7  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Test
test_names = [
    "DEBUG_detect_daily_outliers_sampler_0",
    "DEBUG_detect_daily_outliers_static_cases_0"
]

for test_name in test_names:
    input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
    execute_test(
        test_name=test_name,
        key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
        func=DEBUG_detect_daily_outliers)
    if err:
        raise err


for test_name in test_names:
    input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
    execute_test(
        test_name=test_name,
        key=b'TRvr-9VmaJUp1zd7zrLuAEIuTEsdPcOlG1I8_iQy0xs=',
        func=DEBUG_detect_daily_outliers,
        hidden=True)
    if err:
        raise err


print('Passed! Please submit.')
### END HIDDEN TESTS

Connecting Outliers to the Schedule¶

Exercise 7 flagged the dates when 80-HD racked up an unusual number of errors. Professor Vuduc's schedule records what was happening on each of those dates — exams, instruction days, weekends, holidays, vacation.

Knowing which* dates were meltdown days is not yet an answer. A label that covers most of the calendar will collect outlier days simply by being common, so a raw count of outlier days per label rewards whatever kind of day 80-HD saw most often. What settles the question is the *rate: of all the days carrying a given label, what fraction were meltdown days?

In this exercise you will compute that rate for every label on the schedule and rank the labels by it. The ranking is what finally reveals whether 80-HD's surliness follows a pattern.

Exercise 8: summarize_outliers_by_label (3 points)¶

Your task: define summarize_outliers_by_label as follows:

Summarize how often each kind of scheduled day was an outlier day.

Args:

  • outliers (list): Dates (str, "YYYY-MM-DD") flagged as outliers.
  • schedule (list): Dicts with 'date' (str, "YYYY-MM-DD") and 'label' (str) keys. Each date appears at most once.

Returns:

  • A list of tuples, one per distinct 'label' in schedule: (label, outlier_days, total_days, outlier_rate).

Requirements:

  1. outlier_days is the number of schedule entries carrying that label whose 'date' appears in outliers.
  2. total_days is the number of schedule entries carrying that label.
  3. outlier_rate is round(outlier_days / total_days, 3).
  4. Every label in schedule must appear in the output, including labels with outlier_days of 0.
  5. Ignore any date in outliers that does not appear in schedule. It contributes to nothing.
  6. Each element of the returned list must be a tuple, not a list.
  7. Sort the result by outlier_rate in descending order. Break ties by label in ascending alphabetical order.

Worked example: Suppose outliers = ["2025-02-05", "2025-04-04", "2025-06-30"] and schedule holds two "Exams" days (2025-02-05, 2025-04-04), four "Instruction" days (none of them outliers), and one "Holiday" (not an outlier):

  • "Exams": 2 of 2 days are outliers, so round(2 / 2, 3) = 1.0
  • "Holiday": 0 of 1, so 0.0
  • "Instruction": 0 of 4, so 0.0
  • 2025-06-30 is not in schedule, so it is ignored entirely.
  • Sorted by rate descending, then label ascending: [("Exams", 2, 2, 1.0), ("Holiday", 0, 1, 0.0), ("Instruction", 0, 4, 0.0)]
In [ ]:
### Solution - Exercise 8  
def summarize_outliers_by_label(outliers, schedule):
    ### BEGIN SOLUTION
    outlier_dates = set(outliers)
    total_days = {}
    outlier_days = {}
    for entry in schedule:
        label = entry['label']
        total_days[label] = total_days.get(label, 0) + 1
        if entry['date'] in outlier_dates:
            outlier_days[label] = outlier_days.get(label, 0) + 1

    summary = []
    for label, total in total_days.items():
        flagged = outlier_days.get(label, 0)
        summary.append((label, flagged, total, round(flagged / total, 3)))

    summary.sort(key=lambda row: (-row[3], row[0]))
    return summary
    ### END SOLUTION


### Exercise Demo
outliers_demo = ['2025-02-05', '2025-04-04', '2025-06-30']
schedule_demo = [
    {'date': '2025-01-06', 'label': 'Instruction'},
    {'date': '2025-01-14', 'label': 'Instruction'},
    {'date': '2025-01-16', 'label': 'Instruction'},
    {'date': '2025-01-20', 'label': 'Holiday'},
    {'date': '2025-01-31', 'label': 'Instruction'},
    {'date': '2025-02-05', 'label': 'Exams'},
    {'date': '2025-04-04', 'label': 'Exams'},
]
result = summarize_outliers_by_label(outliers_demo, schedule_demo)
for row in result:
    print(row)  

The demo should display this output:


Demo Function Call

('Exams', 2, 2, 1.0)
('Holiday', 0, 1, 0.0)
('Instruction', 0, 4, 0.0)

The cell below will check your solution for summarize_outliers_by_label (exercise 8).

These variables will be available for debugging under the following names in a dictionary format.

  • input_vars - Input variables for your solution.
  • original_input_vars - Copy of input variables from prior to running your solution.
    • Any key:value pair in original_input_vars should also exist in input_vars - otherwise the inputs were modified by your solution.
  • returned_output_vars - Outputs returned by your solution.
  • true_output_vars - The expected output.
    • This should "match" returned_output_vars based on the question requirements - otherwise, your solution is not returning the correct output.

Note: Unlike the inline demo in the cell where you defined your solution, this check cell is intended to be a complete test of your solution.

  • There is a hidden test cell below which executes the same check for grading.
  • Alert the course staff if the grading fails but the check passes. This is unintended behavior.
In [ ]:
### Check Cell - Exercise 8  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Check
test_name = 'summarize_outliers_by_label_sampler_0'
input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
    func=summarize_outliers_by_label
)
if err:
    raise err


print('Check passed! Please submit.')
In [ ]:
### BEGIN HIDDEN TESTS
### Test Cell - Exercise 8  
from cse6040_devkit.grading.test_cell import execute_test

# Execute Test
test_name = 'summarize_outliers_by_label_sampler_0'

input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'E5OnaX2b3Ot74C-BJrSgYuO2VFGcXawhwRwxx5WcTAs=',
    func=summarize_outliers_by_label)
if err:
    raise err


input_vars, original_input_vars, returned_output_vars, true_output_vars, err = \
execute_test(
    test_name=test_name,
    key=b'TRvr-9VmaJUp1zd7zrLuAEIuTEsdPcOlG1I8_iQy0xs=',
    func=summarize_outliers_by_label,
    hidden=True)
if err:
    raise err

print('Passed! Please submit.')
### END HIDDEN TESTS

Conclusion¶

Having parsed, filtered, cleansed, and analyzed the logs, the data doesn't lie: 80-HD's meltdowns track exams.

The sentiment labels from Exercise 6 tell the same story from a different angle. On meltdown days, 80-HD's messages are negative about three times as often as on ordinary days (roughly 30% versus 9%).

Clearly, the root cause of these system errors isn't hardware failure — it's emotional neglect. To minimize these moods, one might strongly recommend that Professor Vuduc immediately downgrade exam difficulty (or, better yet, drop them entirely). It is the only scientifically sound way to ensure 80-HD gets the un-divided, stress-free attention it deserves.

Fin.¶

If you have made it this far, congratulations! Remember to submit the exam to ensure you receive all the points you have earned!