My attempts to work through the 2021 Advent of Code problems.

```commit 49b32ed7252e5edd87e0a4b426787193bcd5442f
parent 621c531824bbfdf326c1c129cd2296fb05be21a7
Date:   Thu,  2 Dec 2021 12:06:26 -0500

Solution to day 2, part 1

Diffstat:
```
```1 file changed, 72 insertions(+), 0 deletions(-)
diff --git a/day02_part1.py b/day02_part1.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+"""Day 2, Part 1: we need to parse submarine commands and figure out where the
+submarine winds up. I am very confident that I wrote code to solve this exact
+
+# Once again I'm going to take a pandas-heavy approach even though it's not
+# strictly necessary here. I'm going to make some tweaks to the coding
+# conventions I used yesterday; I'd like to make it easier to develop different
+# implementations and compare their performance.
+
+from io import StringIO
+import numpy as np
+import pandas as pd
+from utils import get_puzzle_input
+
+def convert_input_to_df(input_string):
+    """Given a string representation of the input data, return a single-column
+    pandas data frame with the column `commands`"""
+                       names=('commands',))
+
+def split_command_parts(command_df):
+    """Given a pandas data frame with a column `commands`, returns a new data
+    frame with the columns `direction` and `distance`"""
+    return (
+        command_df['commands']
+        .str.split(n=2, expand=True)
+        .rename(columns={0:'direction', 1:'distance'})
+        .assign(distance=lambda x: pd.to_numeric(x['distance']))
+    )
+
+def convert_commands_to_offsets(command_df):
+    """Given a pandas data frame with the columns `direction` (in "forward",
+    "up", and "down") and `distance`, returns a new data frame with the columns
+    `axis` (either "horizontal" or "vertical") and `offset`"""
+    return pd.DataFrame(
+        {'axis': np.where(command_df['direction'].isin(('up', 'down')),
+                          'vertical', 'horizontal'),
+         'offset': np.where(command_df['direction'] == 'up',
+                            -command_df['distance'], command_df['distance'])}
+    )
+
+def find_total_offsets(offset_df):
+    """Given a pandas data frame with the column `axis` and any others, find
+    the sum of other columns grouped by `axis`. (Note that this doesn't
+    restrict itself to the axes 'horizontal' or 'vertical' and it doesn't check
+    the other column names"""
+    # You know, in retrospect it was maybe confusing to call a column 'axis' in
+    # pandas world. Ah well!
+    return offset_df.groupby('axis').sum()
+
+def calculate_puzzle_solution(axis_summary_df):
+    """Given a pandas data frame with the sum of `offset` values along each
+    `axis`, multiply the 'horizontal' and 'vertical' offsets."""
+    return axis_summary_df.loc['horizontal', 'offset'] * \
+        axis_summary_df.loc['vertical', 'offset']
+
+def solve_puzzle():
+    """Return the numeric solution to the puzzle"""
+    return calculate_puzzle_solution(
+        find_total_offsets(
+            convert_commands_to_offsets(
+                split_command_parts(
+                    convert_input_to_df(get_puzzle_input(2))
+                )
+            )
+        )
+    )
+
+if __name__ == "__main__":
+    print("Product of horizontal and vertical offsets:",
+          solve_puzzle())
```