{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "# Project 2 starter: Unclaimed Property at Silicone Motors\n\nACCTG 6155, Fall 2026. Due Sunday, November 1, 11:59 PM MT.\n\n**First, if you are in Colab, File > Save a copy in Drive, and work in your copy.**\n\nThis notebook is optional. It gives you:\n\n- a load cell for every table,\n- a stub for each step in 4.1 of the guide,\n- a place for your Schedule A table, all 50 rows,\n- a last section that lays out every number in 3.4 for your data support file, with your Schedule A table and your window calculation.\n\nThe guide, the counsel memo and the data dictionary are on the [Project 2 page](https://seanmccaman.com/acctg6155/2026-fall/project-2/). The memo is the source for every rule. This notebook repeats a few of its rules where the code needs them, not all of them, so read the memo before section 1.\n\nWhen you are done, run every cell. This notebook, with its outputs showing, can be your data support file, and section 9 at the end says how to export it. The deck and the one-pager are separate files.",
   "id": "p2s-00"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Own your work\n\nI encourage you to use AI in this project, especially to read Schedule A off the scan and to write the SQL and the pandas. Check its work against the memo and the image, not against the AI.\n\nIf an AI writes any of the code in this notebook for you, make it explain what it did before you use it. Keep asking until you can explain it with the AI closed. Then check one number by hand: pick one owner or one row and follow it through. Section 4.5 of the guide says more. You don't turn these explanations in.\n\nA prompt that works, from 4.5 of the guide:\n\n> Here is a [query, formula or method]: [paste it]. It should [say in one sentence what it produces, and which rule from the memo it applies]. Explain what it does, one step at a time, in plain words, as if I have to defend it to an auditor. For each step, tell me why it is there and what would change if I left it out. List every assumption it makes about the data, so I can check each one against the memo. Then tell me how to check one row of my output by hand: which rows of the raw data to pull, and what to add up or compare. Do not make up the numbers. I will run the check myself.\n\n**If I ask you about your work and you cannot explain it to me, you don't own it.**",
   "id": "p2s-01"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 0. Load the data\n\nThe next cell loads every table from the course GitHub repository. If you work in Jupyter on your own machine and unzipped `p2_csv.zip` next to this notebook, it loads from that folder instead. Money is in integer cents. Keep it in cents until you report, because Excel, pandas and SQLite round differently.",
   "id": "p2s-02"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "import os\nimport pandas as pd\nfrom IPython.display import display\n\nBUILD_ID = \"c14c2737ec6d\"   # the data build this notebook expects; the row counts below are the check\nRAW = \"https://raw.githubusercontent.com/sean-mccaman/acctg5150-090/main/2026-fall/project-2/data/csv/\"\nLOCAL = \"p2_csv\"          # the unzipped CSV download, if you have it\n\nTABLES = ['drivers', 'wallet_ledger', 'charging_sessions', 'app_login_days', 'reservations', 'reservation_events', 'company_notices', 'due_diligence_letters', 'gl_wallet_control', 'run_params']\nEXPECTED_ROWS = {'drivers': 8000, 'wallet_ledger': 57916, 'charging_sessions': 39556, 'app_login_days': 32135, 'reservations': 2104, 'reservation_events': 3048, 'company_notices': 12673, 'due_diligence_letters': 422, 'gl_wallet_control': 270, 'run_params': 1}\nTEXT_COLUMNS = [\"state_raw\", \"postal_raw\", \"country_raw\", \"address_line1\", \"city\", \"entered_by\", \"gl_account\"]\n\n\ndef load(name):\n    local = os.path.join(LOCAL, name + \".csv\")\n    df = pd.read_csv(local if os.path.exists(local) else RAW + name + \".csv\",\n                     dtype={c: str for c in TEXT_COLUMNS})\n    for c in df.columns:\n        if c.endswith((\"_on\", \"_date\", \"_end\", \"_open\", \"_close\")):\n            df[c] = pd.to_datetime(df[c])\n    return df\n\n\ndata = {t: load(t) for t in TABLES}\nfor t in TABLES:\n    flag = \"\" if len(data[t]) == EXPECTED_ROWS[t] else \"   <- not the expected count; reload from the Project 2 page\"\n    print(f\"{t:24s}{len(data[t]):>8,d} rows{flag}\")\n\ndrivers      = data[\"drivers\"]\nledger       = data[\"wallet_ledger\"]\nsessions     = data[\"charging_sessions\"]\nlogins       = data[\"app_login_days\"]\nreservations = data[\"reservations\"]\nres_events   = data[\"reservation_events\"]\nnotices      = data[\"company_notices\"]\nletters      = data[\"due_diligence_letters\"]\ngl           = data[\"gl_wallet_control\"]\nparams       = data[\"run_params\"].iloc[0]\n\nCUTOFF = params[\"cutoff_date\"]\nprint(\"\\nCutoff:\", CUTOFF.date(), \"| expected build:\", BUILD_ID)\n\n\ndef dollars(cents):\n    \"\"\"For display only. Do the arithmetic in cents.\"\"\"\n    return f\"${cents / 100:,.2f}\"",
   "id": "p2s-03"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "ledger.head()",
   "id": "p2s-04"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "Prefer SQL? The same tables are in one SQLite file. This cell uses your copy if one is next to the notebook, downloads it otherwise, and runs a query. After that you can use `q(\"...\")` for the rest of the project. If you work in pandas only, you can skip it.",
   "id": "p2s-05"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "import sqlite3\nimport urllib.request\n\nDB_FILE = next((p for p in [\"p2_wallet.sqlite\", os.path.join(\"data\", \"p2_wallet.sqlite\")] if os.path.exists(p)), None)\nif DB_FILE is None:\n    try:\n        urllib.request.urlretrieve(\"https://raw.githubusercontent.com/sean-mccaman/acctg5150-090/main/2026-fall/project-2/data/p2_wallet.sqlite\", \"p2_wallet.sqlite\")\n        DB_FILE = \"p2_wallet.sqlite\"\n    except Exception as e:\n        print(f\"Could not download the database ({e}). Download it from the Project 2 page and put it \"\n              \"next to this notebook, or skip this cell and work in pandas.\")\n\nif DB_FILE:\n    db = sqlite3.connect(DB_FILE)\n\n    def q(sql):\n        return pd.read_sql(sql, db)\n\n    display(q(\"SELECT txn_type, COUNT(*) AS n FROM wallet_ledger GROUP BY txn_type ORDER BY n DESC\"))",
   "id": "p2s-06"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 1. Schedule A and the rule table (4.1 step 1)\n\nDo this before you write any analysis code. Two tables come out of the memo, and they are different things.\n\n- **Your Schedule A table** is the whole schedule: all 50 rows as printed, with a `state_code` column for the two-letter code you mapped each state's full name to. It goes in your data support file in full, as 3.5 of the guide asks.\n- **Your rule table** is what your code reads: one row for each of the nine states where the company has drivers, keyed by the code the data uses, with a column for each rule the memo sets by state.\n\nStart with Schedule A. It is an image on pages 3 and 4 of the memo, and section 3.3 of the guide has a few tricks for getting it into a table. Check every row against the image yourself, not against an AI. Then take the rows you need into the rule table, and check every cell of that against the memo too.",
   "id": "p2s-07"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "STATES = ['AZ', 'CA', 'CO', 'DE', 'NV', 'NY', 'TX', 'UT', 'WA']   # the nine states with drivers, as the data codes them, in Schedule A's order\n\nschedule_a = None   # your Schedule A table: all 50 rows as printed, plus a state_code column\n\n# schedule_a = pd.read_csv(\"schedule_a.csv\")   # for example, your checked extraction, saved next to this notebook\n# schedule_a[\"state_code\"] = ...               # the two-letter code for each state's full name, the way the data writes it",
   "id": "p2s-08"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# This checks that your Schedule A table is all there and that every state with drivers is mapped.\n# It counts rows. It cannot tell you a value is right. Only the image can.\nif schedule_a is None:\n    print(\"No Schedule A table yet.\")\nelse:\n    print(f\"{len(schedule_a)} rows. Schedule A lists 50 states.\")\n    if \"state_code\" not in schedule_a.columns:\n        print(\"No state_code column yet.\")\n    else:\n        codes = schedule_a[\"state_code\"].dropna()\n        unmapped = [s for s in STATES if s not in set(codes)]\n        print(\"Every state with drivers is mapped.\" if not unmapped else f\"Not mapped yet: {unmapped}\")\n        repeated = sorted(set(codes[codes.duplicated()]))\n        if repeated:\n            print(f\"Mapped from more than one row: {repeated}\")\n    display(schedule_a.head())",
   "id": "p2s-09"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**The rule table.** Add a column for each rule the memo sets by state, one row for each code in `STATES`. Put the rules that apply to every state in the cell after it, as plain Python values, so the code below reads them from there instead of retyping a number.\n\nThe memo has a footnote that changes a number.",
   "id": "p2s-10"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "rules = pd.DataFrame(index=pd.Index(STATES, name=\"state\"))\n\n# One column per rule in the memo's schedules. For example:\n# rules[\"wallet_period_years\"] = [...]   # nine values, in the order of STATES\n\nrules",
   "id": "p2s-11"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# Memo-wide rules. Fill these in from the memo.\nOWNER_CONTACT_CHANNELS = set()   # source_channel values that are the owner's own doing\nOWNER_CONTACT_TYPES = set()      # txn_type values that can be owner contact\nFALLBACK_STATE = None            # where a balance reports when the memo's priority rule sends it elsewhere",
   "id": "p2s-12"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 2. Rebuild the balances and tie them to the GL (4.1 step 2; 3.4 items 1 to 3)\n\nSum the ledger per driver as of the cutoff, on the date each transaction happened (`activity_on`), with cash and promotional credit kept apart. The data dictionary lists the transaction types. Then compare your totals with `gl_wallet_control` at June 30.\n\nOne of them will not tie. The difference has a cause you can find in the data, and it is a reconciling item to explain, not an error to plug.",
   "id": "p2s-13"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "CASH_TYPES = set()    # txn_type values that move the owner's cash\nPROMO_TYPES = set()   # txn_type values that move promotional credit\n\n# asof = ledger[ledger[\"activity_on\"] <= CUTOFF]\n# cash_by_driver = ...\n# promo_by_driver = ...\n\ngl[gl[\"period_end\"] == CUTOFF]",
   "id": "p2s-14"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 3. The owner summary table (4.1 step 3)\n\nStart from `drivers`, because it holds the whole population, and bring everything else in with a left join (`how=\"left\"`), so no owner drops out. Summarize each table to one row per owner before you join it, so the join cannot repeat an owner. The goal is one row per owner with these columns: `driver_id`, `clean_state`, jurisdiction, cash balance, promo balance, last owner wallet transaction, last login, last letter response, last owner contact, and mailable. Some columns fill in here and the rest in section 4. Deposits run their own clocks, so build a second table the same way, one row per reservation.\n\nUse the memo's definition of owner contact and nothing else. Two mistakes cost real money here: entries the company generated, and transactions that were later reversed. The ledger links each reversal to the row it backs out. A refund also fills in `reverses_txn_id`, pointing at the charge it corrects, so read the type: only a reversal cancels the row it points to. A reply to a due diligence letter from an earlier cycle is owner contact.",
   "id": "p2s-15"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "reversals = ledger[ledger[\"txn_type\"] == \"reversal\"]\nreversals[[\"txn_id\", \"driver_id\", \"activity_on\", \"amount_cents\", \"source_channel\", \"reverses_txn_id\"]].head()",
   "id": "p2s-16"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "owners = drivers[[\"driver_id\", \"state_raw\"]].copy()   # one row per owner; bring everything else in with how=\"left\"\n\n# owners[\"cash_cents\"] = ...             # cash balance at the cutoff, from section 2\n# owners[\"promo_cents\"] = ...            # promotional credit at the cutoff\n# owners[\"last_wallet_txn\"] = ...        # owner wallet transactions only: no company entries, nothing reversed, on or before the cutoff\n# owners[\"last_login\"] = ...\n# owners[\"last_letter_response\"] = ...   # replies to letters from earlier cycles\n# clean_state, jurisdiction, last_contact and mailable come in section 4\n\nassert owners[\"driver_id\"].is_unique     # rerun after every join: a join that adds rows breaks it\n\ndeposits = reservations[[\"reservation_id\", \"driver_id\"]].copy()   # one row per reservation, with its own clock\n# deposits[\"deposit_cents\"] = ...        # the deposit still held at the cutoff\n# deposits[\"last_contact\"] = ...         # owner-initiated events on that reservation, from res_events\nassert deposits[\"reservation_id\"].is_unique",
   "id": "p2s-17"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 4. Clean the state and resolve the jurisdiction (4.1 step 4)\n\n`state_raw` was typed by people. Look at every spelling before you map anything. The first cell prints each value with `repr`, which shows the quotes, so a stray space that a plain print hides is visible. Put the cleaned code in a new `clean_state` column next to `state_raw`. Never overwrite the raw field, so you can sample rows and check your mapping. A join that silently drops the rows it cannot match is the most common way this analysis goes wrong, so end this section with a check that no owner is left without a jurisdiction.\n\nThen finish the owner summary table. Whether a login counts as contact depends on the jurisdiction, so last owner contact comes after it.",
   "id": "p2s-18"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "drivers[\"state_raw\"].map(repr).value_counts(dropna=False)",
   "id": "p2s-19"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "drivers[\"country_raw\"].value_counts(dropna=False)",
   "id": "p2s-20"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# STATE_MAP = {...}                      # every spelling you saw, to a two-letter code\n# owners[\"clean_state\"] = ...            # a new column next to state_raw; never overwrite state_raw\n# owners[\"jurisdiction\"] = ...           # the memo's priority rule decides this, not the state column alone\n# assert owners[\"jurisdiction\"].notna().all()\n\n# owners[\"last_contact\"] = ...           # the latest owner contact; a login counts only where the jurisdiction's schedule says so\n# owners[\"mailable\"] = ...               # whether this cycle's letter can be mailed; the memo says when it cannot",
   "id": "p2s-21"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 5. Apply the rules (4.1 step 4; 3.4 items 4 to 7)\n\nAdd the dormancy period to the last contact date and compare the result with the cutoff. An anniversary on the cutoff itself is dormant. Schedule A gives periods in years, so pass `add_months` 12 times the years. It counts calendar months the way Excel's `EDATE` does: August 31 plus 6 months is the last day of February. Counting days instead (365 for a year) drifts by a day around leap years, and a day is enough to move an owner across the cutoff.",
   "id": "p2s-22"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "def add_months(dates, k):\n    \"\"\"Add k calendar months to a Series of dates, the way Excel's EDATE does.\"\"\"\n    return pd.to_datetime(dates) + pd.DateOffset(months=k)\n\n\ndef add_months_each(dates, months):\n    \"\"\"Like add_months, when each row has its own number of months (for example, a period by state).\"\"\"\n    out = pd.Series(pd.NaT, index=dates.index, dtype=\"datetime64[ns]\")\n    for k in pd.unique(months.dropna()):\n        rows = months == k\n        out[rows] = add_months(dates[rows], int(k))\n    return out\n\n\nadd_months(pd.Series(pd.to_datetime([\"2023-08-31\", \"2024-02-29\", \"2023-06-30\"])), 6)",
   "id": "p2s-23"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# dormant wallets: count, and reportable amount by state and in total    (3.4 items 4 and 6)\n# dormant deposits: count, and reportable amount by state and in total   (3.4 items 5 and 6)\n# due diligence letters: the list of owners (driver_id) who get one this cycle, and the count   (3.4 item 7)\n#   The list has no bearing on your other numbers: everything is as of June 30,\n#   before any of this year's letters go out.\n\n# Checks worth running as you go: one row per owner (or per reservation) after every join,\n# no wallet from an exempt state in the dormant count, and at most one letter per owner.",
   "id": "p2s-24"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 6. Recommend (4.1 step 5)\n\nThis is the Controller's second question: what to do before the clock runs out. An owner who acts before the dormancy anniversary keeps their money, because their own activity resets the clock. The anniversary is the last owner contact plus the dormancy period, the same date you compared with the cutoff in section 5.\n\nPick a window, add up the money that reaches its anniversary inside it, and say why you chose it. If no window stands out, use the twelve months after the cutoff, through June 30, 2027, the next cycle's cutoff. Then write the recommendation: what the Controller should do about that money before it goes, who does it, by when, and why. The recommendation goes in the deck and the one-pager. The window calculation goes in your data support file, and section 8 prints it.",
   "id": "p2s-25"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "WINDOW_START = CUTOFF                         # the window opens the day after the cutoff\nWINDOW_END = pd.Timestamp(\"2027-06-30\")      # the default, the next cycle's cutoff. Change it if another window stands out.\n\n# The anniversary of each balance that is not dormant at the cutoff, dated the same way as in section 5.\n# owners[\"anniversary\"] = ...\n# deposits[\"anniversary\"] = ...\n# in_window = (anniversary > WINDOW_START) & (anniversary <= WINDOW_END)\n# window_cents = ...                         # the money that reaches its anniversary in the window\n# by_month = ...                             # optional: when in the window it comes due, for a chart",
   "id": "p2s-26"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 7. The deck and the one-pager (4.1 step 6)\n\nThey are separate files, built outside this notebook. Section 4.2 of the guide is the checklist for the deck, and 4.3 is the one for the one-pager. Take every figure from section 8, so the deck, the one-pager and your data support file agree.",
   "id": "p2s-27"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# A chart for the deck, if you make it here. 4.2 of the guide asks for the dormant dollars by state.\n# import matplotlib.pyplot as plt\n# by_state = ...                         # the dormant dollars by state, from section 5\n# ax = by_state.plot(kind=\"bar\", rot=0, figsize=(8, 4.5))\n# ax.set_xlabel(\"State\")\n# ax.set_ylabel(\"Dormant dollars\")\n# ax.set_title(\"A title that states the finding, not the topic\")",
   "id": "p2s-28"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 8. The numbers\n\nFill in every value from your work above, by variable, not by typing a number in. Single dollar values go in `numbers` as cents, and the last cell prints them as dollars to the cent. Items 4 and 5 also ask for tables of the reportable amounts by state, and those are the exception: put them in dollars yourself, dividing the cents by 100 as the last step, so they read in the units 3.4 asks for. Item 7 asks for the list itself: put the driver_ids that get a letter in `letter_list`, and the count in `numbers`.\n\nTwo more things go in your data support file, and the last cell prints both: your Schedule A table from section 1, all 50 rows, and the window calculation from section 6. Money in `window` goes in as cents too.\n\nThen run the last cell. It prints everything in 3.4 order and lists anything still missing.",
   "id": "p2s-29"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "numbers = {\n    \"1   Wallet cash liability at June 30\":                  None,   # cents\n    \"2   GL 2110 minus your rebuilt cash liability\":         None,   # cents\n    \"2   Transactions that explain the difference\":          None,   # count\n    \"2   Cause of the difference\":                           None,   # one sentence\n    \"3   Promotional credit at June 30\":                     None,   # cents\n    \"3   Ties to GL 2115\":                                   None,   # \"yes\" or \"no\"\n    \"4   Dormant wallets\":                                   None,   # count\n    \"4   Reportable wallet cash, total\":                     None,   # cents\n    \"5   Dormant deposits\":                                  None,   # count\n    \"5   Reportable deposits, total\":                        None,   # cents\n    \"6   Reportable to Delaware, both property types\":       None,   # cents\n    \"6   Why each group reports to Delaware\":                None,   # a sentence per group\n    \"7   Due diligence letters this cycle, count\":           None,   # count\n}\nUNITS = {\"1\": \"cents\", \"2   GL\": \"cents\", \"3   Promotional\": \"cents\", \"4   Reportable\": \"cents\",\n         \"5   Reportable\": \"cents\", \"6   Reportable\": \"cents\"}\nLETTER_COUNT = \"7   Due diligence letters this cycle, count\"\n\nletter_list = None        # list: the driver_ids that get a letter this cycle          (item 7)\nwallet_by_state = None    # table: state, dormant wallets, reportable dollars           (item 4)\ndeposit_by_state = None   # table: state, dormant deposits, reportable dollars          (item 5)\n\nwindow = {                # the window calculation, from section 6                     (4.1 step 5)\n    \"Window opens after\":                                WINDOW_START,\n    \"Window closes on\":                                  WINDOW_END,\n    \"Why this window\":                                   None,   # one sentence\n    \"Money that reaches its anniversary in the window\":  None,   # cents\n}",
   "id": "p2s-30"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "import textwrap\nfrom numbers import Integral, Real\nfrom IPython.display import display\n\nprint(f\"Project 2 numbers, data build {BUILD_ID}\\n\")\nmissing = []\nfor label, value in numbers.items():\n    unit = next((u for prefix, u in UNITS.items() if label.startswith(prefix)), None)\n    if value is None:\n        missing.append(label)\n        shown = \"MISSING\"\n    elif unit == \"cents\" and isinstance(value, Real):\n        shown = dollars(value)\n    elif isinstance(value, Integral) and not isinstance(value, bool):\n        shown = f\"{int(value):,}\"\n    else:\n        shown = str(value)\n    print(f\"{label:52s} {shown}\")\n\nprint(\"\\n7   Due diligence letter list (driver_id)\")\nif letter_list is None:\n    missing.append(\"7   Due diligence letter list\")\n    print(\"MISSING\")\nelse:\n    ids = sorted(int(i) for i in letter_list)\n    note = \"\"\n    if numbers.get(LETTER_COUNT) is not None and int(numbers[LETTER_COUNT]) != len(ids):\n        note += \"   <- not the count above\"\n    if len(set(ids)) != len(ids):\n        note += \"   <- the list repeats an owner; a letter goes to each owner once\"\n    print(f\"{len(ids):,} owners{note}\")\n    print(textwrap.fill(\", \".join(str(i) for i in ids), width=100))\n\nfor name, table in [(\"4   Reportable wallet cash by state\", wallet_by_state),\n                    (\"5   Reportable deposits by state\", deposit_by_state)]:\n    print(f\"\\n{name}\")\n    if table is None:\n        missing.append(name)\n        print(\"MISSING\")\n    else:\n        display(table)\n\nprint(\"\\nThe window calculation (4.1 step 5)\")\nfor label, value in window.items():\n    if value is None:\n        missing.append(f\"Window: {label}\")\n        shown = \"MISSING\"\n    elif isinstance(value, pd.Timestamp):\n        shown = str(value.date())\n    elif isinstance(value, Real) and not isinstance(value, bool):\n        shown = dollars(value)\n    else:\n        shown = str(value)\n    print(f\"{label:52s} {shown}\")\n\nprint(\"\\nSchedule A table, all 50 rows, with the state code each maps to\")\nif schedule_a is None:\n    missing.append(\"Schedule A table\")\n    print(\"MISSING\")\nelse:\n    note = \"\" if len(schedule_a) == 50 else \"   <- Schedule A lists 50 states\"\n    if \"state_code\" not in schedule_a.columns:\n        note += \"   <- no state_code column\"\n    print(f\"{len(schedule_a)} rows{note}\")\n    with pd.option_context(\"display.max_rows\", None, \"display.max_columns\", None):\n        display(schedule_a)\n\nprint(\"\\nStill missing:\" if missing else \"\\nEvery 3.4 number, the window and the Schedule A table are present.\")\nfor label in missing:\n    print(\"  \", label.strip())",
   "id": "p2s-31"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 9. Export\n\nRun every cell from the top (in Colab, Runtime > Run all) and check that the outputs are showing. This notebook, with its outputs, is your data support file. Download the notebook itself (in Colab, File > Download > Download .ipynb), or print it to PDF (File > Print, then Save as PDF). Upload it to the data support file question in the Project 2 quiz, with your deck and your one-pager.",
   "id": "p2s-32"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
