{ "cells": [ { "cell_type": "markdown", "id": "adaf08d3", "metadata": { "tags": [] }, "source": [ "# Text Data Extraction: Structuring Unstructured Financial Documents\n", "\n", "**Chapter 4: Fundamental and Alternative Data**\n", "**Docker image**: `ml4t`\n", "**Section Reference**: See Section 4.5 for text dataset engineering concepts\n", "\n", "## Purpose\n", "\n", "Corporate filings contain valuable information locked in unstructured text. This notebook\n", "demonstrates how to extract and structure high-value text blocks (MD&A, Risk Factors) from\n", "10-K and 10-Q filings, creating clean datasets ready for NLP analysis in later chapters.\n", "\n", "## Learning Objectives\n", "\n", "After completing this notebook, you will be able to:\n", "- Navigate SEC EDGAR filing structure\n", "- Extract specific sections from 10-K/10-Q filings (MD&A, Risk Factors)\n", "- Clean and normalize extracted text\n", "- Create point-in-time correct text datasets\n", "- Prepare data for downstream NLP analysis\n", "\n", "## Cross-References\n", "\n", "- **Upstream**: SEC EDGAR filings via `02_sec_filing_explorer.py`\n", "- **Downstream**: Chapter 10 `08_text_feature_evaluation.py` (NLP feature engineering)\n", "- **Related**: `02_sec_filing_explorer.py` (filing access)\n", "\n", "## Key Concepts\n", "\n", "- **10-K**: Annual report with comprehensive company information\n", "- **10-Q**: Quarterly report with interim financial information\n", "- **MD&A**: Management's Discussion and Analysis of Financial Condition\n", "- **Risk Factors**: Required disclosure of material risks\n", "- **Item Numbers**: Standard sections in SEC filings (Item 7 = MD&A)" ] }, { "cell_type": "code", "execution_count": 1, "id": "959cc2b5", "metadata": { "execution": { "iopub.execute_input": "2026-05-16T23:42:29.853419Z", "iopub.status.busy": "2026-05-16T23:42:29.853286Z", "iopub.status.idle": "2026-05-16T23:42:30.362862Z", "shell.execute_reply": "2026-05-16T23:42:30.362342Z" }, "tags": [] }, "outputs": [], "source": [ "\"\"\"Text Data Extraction — extract and structure high-value text blocks from SEC filings for NLP analysis.\"\"\"\n", "\n", "import warnings\n", "\n", "warnings.filterwarnings(\"ignore\")\n", "\n", "import os\n", "import re\n", "from datetime import datetime\n", "from pathlib import Path\n", "\n", "import numpy as np\n", "import polars as pl\n", "from bs4 import BeautifulSoup\n", "from edgar import Company, set_identity" ] }, { "cell_type": "code", "execution_count": 2, "id": "7b2a7030", "metadata": { "execution": { "iopub.execute_input": "2026-05-16T23:42:30.367215Z", "iopub.status.busy": "2026-05-16T23:42:30.367058Z", "iopub.status.idle": "2026-05-16T23:42:30.369174Z", "shell.execute_reply": "2026-05-16T23:42:30.368881Z" }, "tags": [ "parameters" ] }, "outputs": [], "source": [ "# Production defaults — Papermill injects overrides for CI\n", "EDGAR_TICKER = \"AAPL\"\n", "EDGAR_FORM = \"10-K\"" ] }, { "cell_type": "markdown", "id": "0daaf10f", "metadata": { "tags": [] }, "source": [ "## 1. SEC Filing Structure\n", "\n", "10-K filings follow a standard structure:\n", "\n", "| Item | Section | Content |\n", "|------|---------|---------|\n", "| Item 1 | Business | Company description and operations |\n", "| Item 1A | Risk Factors | Material risks to the business |\n", "| Item 7 | MD&A | Management's analysis of financial condition |\n", "| Item 7A | Quantitative Disclosures | Market risk exposures |\n", "| Item 8 | Financial Statements | Audited financials |\n", "\n", "The most valuable sections for NLP analysis:\n", "- **Item 1A (Risk Factors)**: Sentiment, risk themes, changes over time\n", "- **Item 7 (MD&A)**: Management's perspective on performance" ] }, { "cell_type": "code", "execution_count": 3, "id": "b317fde4", "metadata": { "execution": { "iopub.execute_input": "2026-05-16T23:42:30.377937Z", "iopub.status.busy": "2026-05-16T23:42:30.377815Z", "iopub.status.idle": "2026-05-16T23:42:30.381933Z", "shell.execute_reply": "2026-05-16T23:42:30.381575Z" }, "lines_to_next_cell": 2, "tags": [] }, "outputs": [], "source": [ "# Define section patterns for extraction\n", "# Note: 10-K and 10-Q have different item numbers for the same content\n", "# 10-K: MD&A = Item 7, Risk Factors = Item 1A\n", "# 10-Q: MD&A = Item 2 (Part I), Risk Factors = Item 1A (Part II, if updated)\n", "\n", "SECTION_PATTERNS_10K = {\n", " \"risk_factors\": {\n", " # Line-anchored patterns to avoid Table of Contents matches\n", " \"start_patterns\": [\n", " r\"^\\s*ITEM\\s*1A\\.?\\s*[-–—]?\\s*RISK\\s*FACTORS\",\n", " r\"^\\s*Item\\s*1A\\.?\\s*[-–—]?\\s*Risk\\s*Factors\",\n", " ],\n", " \"end_patterns\": [\n", " r\"^\\s*ITEM\\s*1B\\b\",\n", " r\"^\\s*ITEM\\s*2\\b\",\n", " r\"^\\s*Item\\s*1B\\b\",\n", " r\"^\\s*Item\\s*2\\b\",\n", " ],\n", " \"item_number\": \"1A\",\n", " },\n", " \"mda\": {\n", " # Handle both straight and curly apostrophes: ' (U+0027) and ' (U+2019)\n", " \"start_patterns\": [\n", " r\"^\\s*ITEM\\s*7\\.?\\s*[-–—]?\\s*MANAGEMENT(?:'|\\u2019)?S?\\s*DISCUSSION\",\n", " r\"^\\s*Item\\s*7\\.?\\s*[-–—]?\\s*Management(?:'|\\u2019)?s?\\s*Discussion\",\n", " ],\n", " \"end_patterns\": [\n", " r\"^\\s*ITEM\\s*7A\\b\",\n", " r\"^\\s*ITEM\\s*8\\b\",\n", " r\"^\\s*Item\\s*7A\\b\",\n", " r\"^\\s*Item\\s*8\\b\",\n", " ],\n", " \"item_number\": \"7\",\n", " },\n", " \"business\": {\n", " \"start_patterns\": [\n", " r\"^\\s*ITEM\\s*1\\.?\\s*[-–—]?\\s*BUSINESS\",\n", " r\"^\\s*Item\\s*1\\.?\\s*[-–—]?\\s*Business\",\n", " ],\n", " \"end_patterns\": [\n", " r\"^\\s*ITEM\\s*1A\\b\",\n", " r\"^\\s*ITEM\\s*2\\b\",\n", " r\"^\\s*Item\\s*1A\\b\",\n", " r\"^\\s*Item\\s*2\\b\",\n", " ],\n", " \"item_number\": \"1\",\n", " },\n", "}\n", "\n", "# 10-Q patterns differ from 10-K\n", "SECTION_PATTERNS_10Q = {\n", " \"risk_factors\": {\n", " # In 10-Q, risk factor updates appear in Part II, Item 1A\n", " # Line-anchored patterns with MULTILINE support (handled in extract_section)\n", " \"start_patterns\": [\n", " r\"^\\s*ITEM\\s*1A\\.?\\s*[-–—]?\\s*RISK\\s*FACTORS\",\n", " r\"^\\s*Item\\s*1A\\.?\\s*[-–—]?\\s*Risk\\s*Factors\",\n", " ],\n", " \"end_patterns\": [\n", " r\"^\\s*ITEM\\s*1B\\b\",\n", " r\"^\\s*ITEM\\s*2\\b\",\n", " r\"^\\s*Item\\s*2\\b\",\n", " ],\n", " \"item_number\": \"1A\",\n", " # For 10-Q risk factors, prefer matches after PART II\n", " \"require_after\": r\"PART\\s*II\",\n", " },\n", " \"mda\": {\n", " # In 10-Q, MD&A is Part I, Item 2 (NOT Item 7)\n", " # Handle both straight and curly apostrophes\n", " \"start_patterns\": [\n", " r\"^\\s*ITEM\\s*2\\.?\\s*[-–—]?\\s*MANAGEMENT(?:'|\\u2019)?S?\\s*DISCUSSION\",\n", " r\"^\\s*Item\\s*2\\.?\\s*[-–—]?\\s*Management(?:'|\\u2019)?s?\\s*Discussion\",\n", " ],\n", " \"end_patterns\": [\n", " r\"^\\s*ITEM\\s*3\\b\",\n", " r\"^\\s*Item\\s*3\\b\",\n", " ],\n", " \"item_number\": \"2\",\n", " },\n", "}\n", "\n", "# Default to 10-K patterns (backward compatible)\n", "SECTION_PATTERNS = SECTION_PATTERNS_10K\n", "\n", "\n", "def get_patterns_for_form(form_type: str) -> dict:\n", " \"\"\"Get appropriate section patterns based on filing type.\"\"\"\n", " form_type_upper = form_type.upper() if form_type else \"10-K\"\n", " if \"10-Q\" in form_type_upper:\n", " return SECTION_PATTERNS_10Q\n", " return SECTION_PATTERNS_10K" ] }, { "cell_type": "markdown", "id": "c6b9fe4a", "metadata": { "lines_to_next_cell": 2, "tags": [] }, "source": [ "## 2. Text Cleaning Functions\n", "\n", "Raw SEC filings contain HTML, special characters, and formatting that must be cleaned." ] }, { "cell_type": "code", "execution_count": 4, "id": "7da03d4e", "metadata": { "execution": { "iopub.execute_input": "2026-05-16T23:42:30.388405Z", "iopub.status.busy": "2026-05-16T23:42:30.388225Z", "iopub.status.idle": "2026-05-16T23:42:30.391214Z", "shell.execute_reply": "2026-05-16T23:42:30.390834Z" }, "tags": [] }, "outputs": [], "source": [ "def clean_html(html_text: str, drop_tables: bool = False) -> str:\n", " \"\"\"Remove HTML tags and convert to plain text while preserving paragraph boundaries.\"\"\"\n", " if not html_text:\n", " return \"\"\n", "\n", " soup = BeautifulSoup(html_text, \"html.parser\")\n", "\n", " # Remove script and style elements\n", " for script in soup([\"script\", \"style\"]):\n", " script.decompose()\n", "\n", " # Optionally remove tables (embedded financial tables in narrative sections)\n", " if drop_tables:\n", " for table in soup.find_all(\"table\"):\n", " table.decompose()\n", "\n", " # Preserve paragraph boundaries by using newline separator\n", " text = soup.get_text(separator=\"\\n\")\n", "\n", " return text" ] }, { "cell_type": "code", "execution_count": 5, "id": "cc7ac3e0", "metadata": { "execution": { "iopub.execute_input": "2026-05-16T23:42:30.394871Z", "iopub.status.busy": "2026-05-16T23:42:30.394712Z", "iopub.status.idle": "2026-05-16T23:42:30.397931Z", "shell.execute_reply": "2026-05-16T23:42:30.397610Z" }, "tags": [] }, "outputs": [], "source": [ "def clean_text(text: str) -> str:\n", " \"\"\"\n", " Clean extracted text while preserving paragraph breaks.\n", "\n", " Removes common filing artifacts and normalizes whitespace without\n", " flattening everything into a single line (preserves paragraph structure\n", " for downstream chunking and change detection).\n", " \"\"\"\n", " if not text:\n", " return \"\"\n", "\n", " # Normalize line endings\n", " text = text.replace(\"\\r\\n\", \"\\n\").replace(\"\\r\", \"\\n\")\n", "\n", " # Remove common filing artifacts (line-anchored for safety)\n", " text = re.sub(r\"(?im)^\\s*table of contents\\s*$\", \"\", text)\n", " text = re.sub(r\"(?im)^\\s*page\\s+\\d+\\s*$\", \"\", text)\n", " text = re.sub(r\"(?im)^\\s*\\d+\\s*of\\s*\\d+\\s*$\", \"\", text)\n", "\n", " # Remove URLs\n", " text = re.sub(r\"https?://\\S+\", \"\", text)\n", "\n", " # Remove excessive special characters but keep newlines\n", " text = re.sub(r\"[_=\\-]{3,}\", \" \", text)\n", " text = re.sub(r\"[•●◦▪]\", \" \", text)\n", "\n", " # Normalize spaces within each line, but preserve line breaks\n", " text = \"\\n\".join(re.sub(r\"[ \\t]+\", \" \", line).strip() for line in text.split(\"\\n\"))\n", "\n", " # Collapse excessive blank lines to paragraph breaks\n", " text = re.sub(r\"\\n{3,}\", \"\\n\\n\", text).strip()\n", "\n", " return text" ] }, { "cell_type": "code", "execution_count": 6, "id": "d33bc058", "metadata": { "execution": { "iopub.execute_input": "2026-05-16T23:42:30.401595Z", "iopub.status.busy": "2026-05-16T23:42:30.401460Z", "iopub.status.idle": "2026-05-16T23:42:30.407030Z", "shell.execute_reply": "2026-05-16T23:42:30.406614Z" }, "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Sample cleaning:\n", "Original HTML length: 228\n", "Cleaned text: ITEM 1A. RISK FACTORS\n", "\n", "The following risks could\n", "materially\n", "affect our business:\n", "\n", "Competition may increase\n", "\n", "Economic conditions may deteriorate\n", "\n", "Page 15 of 120\n" ] } ], "source": [ "def normalize_for_analysis(text: str) -> str:\n", " \"\"\"\n", " Normalize text for NLP analysis.\n", "\n", " Converts to lowercase, removes numbers (optional),\n", " and standardizes formatting.\n", " \"\"\"\n", " if not text:\n", " return \"\"\n", "\n", " # Convert to lowercase\n", " text = text.lower()\n", "\n", " # Standardize common abbreviations\n", " text = re.sub(r\"\\bu\\.s\\.?\\b\", \"united states\", text)\n", " text = re.sub(r\"\\be\\.g\\.?\\b\", \"for example\", text)\n", " text = re.sub(r\"\\bi\\.e\\.?\\b\", \"that is\", text)\n", "\n", " # Remove possessive 's\n", " text = re.sub(r\"'s\\b\", \"\", text)\n", "\n", " # Remove numbers (optional - depends on use case)\n", " # text = re.sub(r\"\\d+\", \"\", text)\n", "\n", " return text\n", "\n", "\n", "# Test cleaning\n", "sample_html = \"\"\"\n", "
ITEM 1A. RISK FACTORS
\n", "The following risks could materially affect our business:
\n", "• Competition may increase
\n", "• Economic conditions may deteriorate
\n", "Page 15 of 120
\n", "Table of Contents
\n", "\"\"\"\n", "\n", "cleaned = clean_text(clean_html(sample_html))\n", "print(\"Sample cleaning:\")\n", "print(f\"Original HTML length: {len(sample_html)}\")\n", "print(f\"Cleaned text: {cleaned}\")" ] }, { "cell_type": "markdown", "id": "b1c3e33b", "metadata": { "lines_to_next_cell": 2, "tags": [] }, "source": [ "## 3. Section Extraction" ] }, { "cell_type": "code", "execution_count": 7, "id": "69aba95d", "metadata": { "execution": { "iopub.execute_input": "2026-05-16T23:42:30.417832Z", "iopub.status.busy": "2026-05-16T23:42:30.417637Z", "iopub.status.idle": "2026-05-16T23:42:30.428151Z", "shell.execute_reply": "2026-05-16T23:42:30.427688Z" }, "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "BUSINESS (728 chars):\n", "Our company is a leading provider of technology solutions for enterprise\n", "customers worldwide. We design, manufacture, and support a portfolio of\n", "hardware, software, and cloud services that help organi...\n", "\n", "RISK_FACTORS (541 chars):\n", "An investment in our securities involves a high degree of risk.\n", "You should carefully consider the following risk factors:\n", "\n", "COMPETITION RISKS\n", "We face significant competition from larger companies with ...\n", "\n", "MDA (424 chars):\n", "AND ANALYSIS OF FINANCIAL CONDITION\n", "\n", "Overview\n", "The past year has been transformative for our company.\n", "Revenue increased 25% driven by strong product adoption.\n", "\n", "Results of Operations\n", "Our gross margin im...\n", "\n" ] } ], "source": [ "def extract_section(text: str, section_name: str, patterns: dict = None) -> str | None:\n", " \"\"\"\n", " Extract a specific section from a 10-K/10-Q filing using anchored item boundaries.\n", "\n", " Robust to TOC duplicates by finding all start_pattern matches and\n", " picking the candidate (start, end) span with the most content\n", " between it and the next end_pattern. Inline references inside\n", " narrative text are filtered out by the same span-length test —\n", " they don't have a matching end-pattern Item N+1 immediately after.\n", "\n", " Parameters\n", " ----------\n", " text : str\n", " Full filing text\n", " section_name : str\n", " Name of section to extract (e.g., 'risk_factors', 'mda')\n", " patterns : dict\n", " Custom patterns, or use SECTION_PATTERNS\n", "\n", " Returns\n", " -------\n", " str or None\n", " Extracted section text, or None if no candidate has substantive content.\n", " \"\"\"\n", " if not text:\n", " return None\n", "\n", " if patterns is None:\n", " patterns = SECTION_PATTERNS\n", "\n", " if section_name not in patterns:\n", " raise ValueError(f\"Unknown section: {section_name}\")\n", "\n", " config = patterns[section_name]\n", " flags = re.IGNORECASE | re.MULTILINE | re.DOTALL\n", "\n", " # Some patterns may require matching after a specific marker\n", " # (e.g., PART II for 10-Q risk factors).\n", " offset = 0\n", " search_text = text\n", " require_after = config.get(\"require_after\")\n", " if require_after:\n", " after_match = re.search(require_after, search_text, flags)\n", " if after_match:\n", " offset = after_match.start()\n", " search_text = search_text[after_match.start() :]\n", "\n", " # Collect every candidate start position from every start pattern.\n", " starts: list[int] = []\n", " for pattern in config[\"start_patterns\"]:\n", " starts.extend(offset + m.end() for m in re.finditer(pattern, search_text, flags))\n", " if not starts:\n", " return None\n", " starts = sorted(set(starts))\n", "\n", " # For each candidate start, find the next end_pattern match and\n", " # measure the span. The real section beats every TOC entry on\n", " # span length because TOC entries are followed by the next TOC\n", " # entry within a few dozen characters.\n", " best_span: tuple[int, int] | None = None\n", " for start_pos in starts:\n", " end_pos = len(text)\n", " for end_pattern in config[\"end_patterns\"]:\n", " end_match = re.search(end_pattern, text[start_pos:], flags)\n", " if end_match:\n", " end_pos = start_pos + end_match.start()\n", " break\n", " span = end_pos - start_pos\n", " if best_span is None or span > (best_span[1] - best_span[0]):\n", " best_span = (start_pos, end_pos)\n", "\n", " if best_span is None or (best_span[1] - best_span[0]) < 200:\n", " return None\n", "\n", " return clean_text(text[best_span[0] : best_span[1]])\n", "\n", "\n", "# Create sample 10-K text for demonstration\n", "sample_10k = \"\"\"\n", "FORM 10-K\n", "ANNUAL REPORT\n", "\n", "Table of Contents\n", "\n", "PART I\n", "\n", "ITEM 1. BUSINESS\n", "\n", "Our company is a leading provider of technology solutions for enterprise\n", "customers worldwide. We design, manufacture, and support a portfolio of\n", "hardware, software, and cloud services that help organizations modernize\n", "their data infrastructure.\n", "\n", "We operate across three reportable segments — Cloud Platform, Enterprise\n", "Software, and Professional Services — and sell directly through a global\n", "sales force as well as a network of channel partners. Our customers span\n", "financial services, healthcare, public sector, and manufacturing.\n", "\n", "Recent strategic initiatives include expanding our hyperscale data center\n", "footprint, deepening integrations with leading public cloud providers,\n", "and broadening our generative-AI product portfolio.\n", "\n", "ITEM 1A. RISK FACTORS\n", "\n", "An investment in our securities involves a high degree of risk.\n", "You should carefully consider the following risk factors:\n", "\n", "COMPETITION RISKS\n", "We face significant competition from larger companies with more resources.\n", "New entrants may disrupt our market position.\n", "\n", "ECONOMIC RISKS\n", "Economic downturns could reduce customer spending on our products.\n", "Currency fluctuations may impact our international revenue.\n", "\n", "REGULATORY RISKS\n", "Changes in regulations could increase our compliance costs.\n", "Data privacy laws continue to evolve across jurisdictions.\n", "\n", "ITEM 1B. UNRESOLVED STAFF COMMENTS\n", "\n", "None.\n", "\n", "ITEM 2. PROPERTIES\n", "\n", "We lease our headquarters in San Francisco, California.\n", "\n", "PART II\n", "\n", "ITEM 7. MANAGEMENT'S DISCUSSION AND ANALYSIS OF FINANCIAL CONDITION\n", "\n", "Overview\n", "The past year has been transformative for our company.\n", "Revenue increased 25% driven by strong product adoption.\n", "\n", "Results of Operations\n", "Our gross margin improved to 72% from 68% in the prior year.\n", "Operating expenses remained well controlled.\n", "\n", "Liquidity and Capital Resources\n", "We ended the year with $500 million in cash.\n", "We believe current resources are sufficient for operations.\n", "\n", "ITEM 7A. QUANTITATIVE AND QUALITATIVE DISCLOSURES ABOUT MARKET RISK\n", "\n", "We are exposed to interest rate and foreign currency risks.\n", "\n", "ITEM 8. FINANCIAL STATEMENTS\n", "\"\"\"\n", "\n", "for section in [\"business\", \"risk_factors\", \"mda\"]:\n", " extracted = extract_section(sample_10k, section)\n", " if extracted:\n", " preview = extracted[:200] + \"...\" if len(extracted) > 200 else extracted\n", " print(f\"{section.upper()} ({len(extracted)} chars):\")\n", " print(preview)\n", " print()" ] }, { "cell_type": "markdown", "id": "438d5831", "metadata": { "lines_to_next_cell": 2, "tags": [] }, "source": [ "## 4. Building a Text Dataset" ] }, { "cell_type": "code", "execution_count": 8, "id": "0b5c7317", "metadata": { "execution": { "iopub.execute_input": "2026-05-16T23:42:30.438564Z", "iopub.status.busy": "2026-05-16T23:42:30.438490Z", "iopub.status.idle": "2026-05-16T23:42:30.445368Z", "shell.execute_reply": "2026-05-16T23:42:30.444964Z" }, "tags": [] }, "outputs": [ { "data": { "text/html": [ "| cik | company_name | accession_no | filing_type | filing_date | accepted_at | period_end | section | text | text_length | word_count |
|---|---|---|---|---|---|---|---|---|---|---|
| str | str | str | str | datetime[μs] | datetime[μs] | datetime[μs] | str | str | i64 | i64 |
| "0000320193" | "Apple Inc." | "0000320193-24-000081" | "10-K" | 2024-10-31 00:00:00 | 2024-10-31 16:35:12 | 2024-09-28 00:00:00 | "risk_factors" | "An investment in our securitie… | 541 | 74 |
| "0000320193" | "Apple Inc." | "0000320193-24-000081" | "10-K" | 2024-10-31 00:00:00 | 2024-10-31 16:35:12 | 2024-09-28 00:00:00 | "mda" | "AND ANALYSIS OF FINANCIAL COND… | 424 | 64 |
| "0000789019" | "Microsoft Corporation" | "0000789019-24-000042" | "10-K" | 2024-07-30 00:00:00 | 2024-07-30 16:05:45 | 2024-06-30 00:00:00 | "risk_factors" | "An investment in our securitie… | 541 | 74 |
| "0000789019" | "Microsoft Corporation" | "0000789019-24-000042" | "10-K" | 2024-07-30 00:00:00 | 2024-07-30 16:05:45 | 2024-06-30 00:00:00 | "mda" | "AND ANALYSIS OF FINANCIAL COND… | 424 | 64 |
| company_name | section | word_count | quality_flag |
|---|---|---|---|
| str | str | i64 | str |
| "Apple Inc." | "risk_factors" | 74 | "too_short" |
| "Apple Inc." | "mda" | 64 | "too_short" |
| "Microsoft Corporation" | "risk_factors" | 74 | "too_short" |
| "Microsoft Corporation" | "mda" | 64 | "too_short" |
| section | extracted | char_count | word_count |
|---|---|---|---|
| str | bool | i64 | i64 |
| "business" | true | 16043 | 2332 |
| "risk_factors" | true | 68195 | 9836 |
| "mda" | true | 18569 | 2762 |