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

```commit 235c28b62909ff016963880a7034ac465ee440df
parent 49b32ed7252e5edd87e0a4b426787193bcd5442f
Date:   Thu,  2 Dec 2021 14:09:09 -0500

Solution to day 2, part 2

Diffstat:
```
```1 file changed, 69 insertions(+), 0 deletions(-)
diff --git a/day02_part2.py b/day02_part2.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+"""Day 2, Part 2: It turns out that controlling the submarine is more
+complicated than we thought in Part 1. We need to maintain a third state
+variable (in addition to horizontal and vertical position) called 'aim', and
+use that when calculating sub movement."""
+
+# Hey, the functions from Part 1 will be useful once again. All the input
+# parsing is the same, and now we just need to use slightly more complicated
+# logic when figuring out final position. We can start from the point where we
+# have a data frame with the columns `axis` and `offset`.
+
+#from io import StringIO
+import numpy as np
+import pandas as pd
+from utils import get_puzzle_input
+from day02_part1 import (convert_input_to_df,
+                         split_command_parts,
+                         convert_commands_to_offsets)
+
+def find_submarine_state(offset_df):
+    """Given a pandas data frame with the columns `axis` and `offset`, we track
+    `aim` as well as `horizontal` and `vertical` offsets, using complicated
+    rules:
+    * 'vertical X' increases or decreases aim (which starts at 0)
+    * 'horizontal X':
+      * increases horizontal position by X
+      * increases vertical position (i.e., depth) by aim * X
+    This returns a data frame with the columns `horizontal`, `vertical` and
+    `aim`, representing the running value of these states."""
+    return (
+        pd.DataFrame(
+            # 'aim' represents the up-to-date state of the aim parameter, while
+            # 'horizontal_step' is just the current step's offset. We save this
+            # intermediate calculation because we need it for 'depth'.
+            # This line uses the extremely new-to-me 'walrus operator' to
+            # cretae the variable `vert_rows` so we don't calculate the
+            {'aim': np.where(vert_rows := offset_df['axis'] == 'vertical',
+                             offset_df['offset'], 0).cumsum(),
+             'horizontal_step': np.where(~vert_rows,
+                                         offset_df['offset'], 0)}
+        )
+        .assign(depth=lambda x: (x['aim'] * x['horizontal_step']).cumsum(),
+                horizontal=lambda x: x['horizontal_step'].cumsum())
+        .drop('horizontal_step', axis='columns')
+    )
+
+def calculate_puzzle_solution(running_state_df):
+    """Given a pandas data frame with the running values of the state of the
+    submarine (representing the `horizontal` offset, the `depth`, and also the
+    `aim` which is not used), return the product of the final value of the
+    `horizontal` offset and `depth`"""
+    return running_state_df.iloc[-1][['depth', 'horizontal']].prod()
+
+def solve_puzzle():
+    """Return the numeric solution to the puzzle"""
+    return calculate_puzzle_solution(
+        find_submarine_state(
+            convert_commands_to_offsets(
+                split_command_parts(
+                    convert_input_to_df(get_puzzle_input(2))
+                )
+            )
+        )
+    )
+
+if __name__ == "__main__":
+    print("Product of (new) horizontal and vertical offsets:",
+          solve_puzzle())
```