Table of Contents
- Module 1 — Using AI Tools for Exploratory Data Analysis
- Module 2 — Using AI Tools for Analyzing Your Data
- Module 3 — Using AI Tools for Generating Analytical Reports
- Overall Workflow
- Key Concepts and Best Practices
- Tools and Resources
Overall Workflow
The following diagram illustrates the complete workflow for this course, from data identification through report automation:
flowchart TD
A([🚀 Project Start]) --> B["Identify data source\nwith ChatGPT"]
B --> C["Download BLS data\nQCEW - Quarterly Census of Employment and Wages"]
C --> D["Create a 1,000-row sample\nfor EDA"]
D --> E["Send sample to OpenAI API\nvia eda_pipeline.py"]
E --> F["Generated EDA report\ncolumns, types, stats, quality, insights"]
F --> G["Use Grok to identify\nthe 50 AI-vulnerable sectors"]
G --> H["Send EDA report + schema + sectors\nto Claude to generate analysis code"]
H --> I{Analysis type}
I -->|Time series| J["Time Series Analysis\nTimeSeries.ipynb"]
I -->|Distribution| K["Distribution Analysis\ndist_analysis_gpt.py"]
I -->|Cohorts| L["Cohort Analysis\nanalysis_pipeline.py"]
I -->|Statistical tests| M["Hypothesis Testing\nANOVA, t-test, chi-square..."]
J --> N[PDF Report Generation]
K --> N
L --> N
M --> N
N --> O["Second AI model verification\nFact-checking with ChatGPT"]
O --> P{Valid data?}
P -->|No| Q["Fix the code\nand re-run"]
Q --> J
P -->|Yes| R["Automation with parameterized template\ndemos.ipynb"]
R --> S[Version control with Git]
S --> T([✅ Final automated report])
Module 1 — Using AI Tools for Exploratory Data Analysis
Module Overview
mindmap
root((Module 1 EDA with AI))
Identify data
ChatGPT for suggestions
BLS - Bureau of Labor Statistics
QCEW - quarterly data
Understand structure
36 million rows
38 columns
Data schema
Automated EDA
1,000-row sample
OpenAI API
Markdown report
Advanced analysis
Distribution analysis
Correlation matrices
Multicollinearity
Heteroskedasticity
Understanding Your Data
Large datasets can be intimidating, especially when trying to mentally visualize column headers and data ranges. Generative AI models excel at this task, making it relevant to delegate Exploratory Data Analysis (EDA) work to them.
Identifying the Right Data Source
Even before starting the analysis, AI can help identify the right data source:
Prompt → ChatGPT: "Which Bureau of Labor Statistics datasets
allow studying the impact of AI on the labor market?"
Response → List of resources including:
• BLS Quarterly Census of Employment and Wages (QCEW)
• Links to download pages
• Links to column schemas
The selected source: BLS QCEW — quarterly census of employment and wages.
BLS QCEW Dataset Structure
| Characteristic | Value |
|---|---|
| Number of rows | ~36 million |
| Number of columns | 38 |
| Format | CSV |
| Frequency | Quarterly / Annual |
| Coverage | United States, by industrial sector |
⚠️ Important limitation: A CSV file of this size is too large to be uploaded directly into an AI model. You need to create a representative sample (e.g., 1,000 rows) before sending to the model.
Main Dataset Columns
area_fips → FIPS code for geographic area
own_code → Owner type code (public/private)
industry_code → NAICS industry sector code
agglvl_code → Aggregation level
size_code → Establishment size
year → Year
qtr → Quarter (1-4, A for annual)
disclosure_code → Disclosure code (N = not disclosed)
annual_avg_estabs → Average annual number of establishments
annual_avg_emplvl → Average annual employment level
total_annual_wages→ Total annual wages
annual_avg_wkly_wage → Average annual weekly wage
avg_annual_pay → Average annual pay
EDA Code — Sample Preparation (eda_pipeline.py)
The eda_pipeline.py script is the core of automated EDA. It uses the OpenAI API to analyze a sample of the dataset:
import os
import pandas as pd
from dotenv import load_dotenv
from openai import OpenAI
from IPython.display import Markdown, display
# Load credentials from .env file
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
# --- Configuration ---
CSV_FILE = "2024.annual.singlefile.csv"
SAMPLE_SIZE = 1000
# --- Step 1: Load and sample the CSV ---
df_full = pd.read_csv(CSV_FILE, low_memory=False)
df_sample = df_full.sample(n=SAMPLE_SIZE, random_state=42)
# --- Step 2: Build the data context for AI ---
summary = {
"shape": df_sample.shape,
"columns": list(df_sample.columns),
"dtypes": df_sample.dtypes.astype(str).to_dict(),
"head": df_sample.head(5).to_string(),
"describe": df_sample.describe(include="all").to_string(),
"null_counts": df_sample.isnull().sum().to_dict(),
}
# --- Step 3: Build the EDA prompt ---
eda_prompt = f"""
You are an expert data scientist. Perform a comprehensive
Exploratory Data Analysis (EDA) on the following dataset sample.
Dataset shape: {summary['shape']}
Columns: {summary['columns']}
Data types: {summary['dtypes']}
Sample data:
{summary['head']}
Descriptive statistics:
{summary['describe']}
Missing values:
{summary['null_counts']}
Please provide:
1. Column descriptions and interpretation
2. Data quality issues (missing values, outliers, anomalies)
3. Key statistical insights
4. Distribution characteristics of numerical columns
5. Assumptions about the meaning and context of the data
6. Recommendations for further analysis
7. What insights could be extracted from the full dataset
"""
# --- Step 4: Call the API and display the report ---
def run_eda(csv_file: str, sample_size: int = 1000):
"""Run full EDA and save the report."""
print(f"Loading {csv_file} (sample of {sample_size} rows)...")
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "You are an expert data scientist specializing in "
"labor market analysis and large dataset exploration."
},
{"role": "user", "content": eda_prompt}
],
temperature=0.3,
max_tokens=4000,
)
report = response.choices[0].message.content
# Display in notebook
display(Markdown(report))
# Save report to file
report_filename = f"eda_report_{csv_file.replace('.csv', '')}.md"
with open(report_filename, "w", encoding="utf-8") as f:
f.write(report)
print(f"\nReport saved: {report_filename}")
return report_filename
# --- Execution ---
report_file = run_eda(CSV_FILE, sample_size=SAMPLE_SIZE)
Sample EDA Report Output
## EDA Report — BLS QCEW Annual Dataset (Sample: 1,000 rows)
### Column Overview
- area_fips: FIPS codes for geographic areas (string, e.g., "01001")
- industry_code: NAICS industry codes (string, can be ranges)
- avg_annual_pay: Average annual pay per worker ($)
### Data Quality Issues
- disclosure_code = 'N': ~15% of rows — wages suppressed for privacy
- industry_code: Some entries are ranges (e.g., "10-") — handle carefully
- Null values in lq_* columns: ~8%
### Key Statistical Insights
- avg_annual_pay ranges from $18,000 to $6,200,000+
- Extreme right skew — median ($52,000) << mean ($68,000)
- annual_avg_emplvl: bimodal distribution (small vs large employers)
### Recommendations
1. Filter disclosure_code != 'N' before statistical analysis
2. Use median/geometric mean rather than mean for salary analysis
3. Aggregate to national level (area_fips = "US000") for trends
4. Cross-reference industry_code with NAICS classification table
Finding Patterns in Your Data
Strategy: Using the EDA Report as Context
Rather than manually reading and analyzing the EDA report, the optimal strategy is to use it as context for other AI prompts:
flowchart LR
A["EDA Report\n.md"] --> B{New AI prompt}
C["BLS Schema\n.csv"] --> B
D["50 vulnerable\nsectors .md"] --> B
B --> E["Claude generates\nanalysis code"]
E --> F["Jupyter Notebook\nready to execute"]
Identifying AI-Vulnerable Sectors with Grok
To identify the industrial sectors most likely to be disrupted by AI, a query is sent to Grok (known for its strong real-world awareness):
Prompt → Grok:
"Here is a CSV containing all NAICS industry codes.
Identify the 50 sectors most likely to be impacted
by AI adoption, based on existing discussions
about technological disruption."
[Upload BLS industrial code CSV file]
Grok returns a list of 50 sectors in Markdown format, including:
- Financial and banking services
- Accounting and bookkeeping
- Legal services (research work)
- Insurance claims processing
- Data entry and administrative processing
- Medical transcription
- Translation services
- Journalism and content writing
- Customer support services (call centers)
- Medical radiology (image reading)
Building the Complete Analysis Prompt
The prompt sent to Claude combines three files:
Uploaded files:
1. eda_report_2024.annual.singlefile.md → Full dataset context
2. field_layouts.csv → BLS column schema
3. ai_vulnerable_industries.md → The 50 at-risk sectors
Prompt:
"I have a Python Jupyter Notebook using pandas.
Generate code to analyze BLS 2023 and 2024 datasets,
identifying trends that suggest AI's impact
on job availability and wages.
Focus on the 50 sectors identified as AI-vulnerable."
Advanced AI Model Queries
Automated Distribution Analysis
Distribution analysis determines which statistical distribution best describes the behavior of a data column. AI can automate complex tests:
# dist_analysis_gpt.py
# Generated by ChatGPT from a prompt including the EDA report
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as stats
from scipy.stats import norm, lognorm, expon, pareto
# Load data
df = pd.read_csv("2024.annual.singlefile.csv", low_memory=False)
df = df[df['disclosure_code'] != 'N']
pay_data = df['avg_annual_pay'].dropna()
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 1. Histogram + KDE (Kernel Density Estimate)
axes[0, 0].hist(pay_data, bins=100, density=True, alpha=0.6, color='steelblue')
pay_data.plot.kde(ax=axes[0, 0], color='red')
axes[0, 0].set_title("Average Annual Pay Distribution — Histogram + KDE")
axes[0, 0].set_xlabel("Average Annual Pay ($)")
# 2. Box plot
axes[0, 1].boxplot(pay_data, vert=True)
axes[0, 1].set_title("Box Plot — Average Annual Pay")
axes[0, 1].set_ylabel("Average Annual Pay ($)")
# 3. Q-Q plot (normality test)
stats.probplot(pay_data, dist="norm", plot=axes[1, 0])
axes[1, 0].set_title("Q-Q Plot (normal distribution)")
# 4. Distribution comparison
x = np.linspace(pay_data.min(), pay_data.quantile(0.99), 200)
for dist_name, dist in [("Normal", norm), ("Log-Normal", lognorm), ("Pareto", pareto)]:
params = dist.fit(pay_data)
axes[1, 1].plot(x, dist.pdf(x, *params), label=dist_name)
axes[1, 1].set_title("Candidate Distribution Comparison")
axes[1, 1].legend()
plt.tight_layout()
plt.savefig("distribution_analysis.png", dpi=120)
plt.show()
# Formal statistical tests
print("\n=== Distribution Tests ===")
_, p_normal = stats.normaltest(pay_data.sample(5000))
print(f"Normality test (D'Agostino-Pearson) — p-value: {p_normal:.4e}")
_, p_shapiro = stats.shapiro(pay_data.sample(5000))
print(f"Shapiro-Wilk test — p-value: {p_shapiro:.4e}")
print("\n=== Descriptive Statistics ===")
print(f"Median: ${pay_data.median():,.0f}")
print(f"Mean: ${pay_data.mean():,.0f}")
print(f"Skewness: {pay_data.skew():.2f}")
print(f"Kurtosis: {pay_data.kurtosis():.2f}")
Interpreting Distribution Results
Observed results:
• The distribution is extremely right-skewed (high skewness)
• A few sectors (investment banking, portfolio management,
professional sports) generate salaries > $6M
• The vast majority of sectors are concentrated on the left (< $100K)
Statistical conclusion:
• Do NOT use methods based on normal distribution
• Recommended:
- Pareto distribution for extreme values
- Median or geometric mean for the main population
- Non-parametric tests (Mann-Whitney U, Kruskal-Wallis)
Available Analysis Alternatives
graph TD
A["Analysis alternatives\nvia AI prompts"] --> B["Correlation Matrices"]
A --> C["Multicollinearity"]
A --> D["Heteroskedasticity"]
B --> E["df.corr() + seaborn heatmap\nIdentify related variables"]
C --> F["VIF - Variance Inflation Factor\nAvoid redundancy in models"]
D --> G["Breusch-Pagan Test\nNon-constant residual variance"]
Example — Correlation matrix:
import seaborn as sns
import matplotlib.pyplot as plt
numeric_cols = [
'annual_avg_estabs', 'annual_avg_emplvl',
'total_annual_wages', 'annual_avg_wkly_wage', 'avg_annual_pay'
]
corr_matrix = df[numeric_cols].corr()
plt.figure(figsize=(10, 8))
sns.heatmap(
corr_matrix,
annot=True, fmt=".2f",
cmap="coolwarm", center=0,
square=True
)
plt.title("Correlation Matrix — BLS QCEW Metrics")
plt.tight_layout()
plt.savefig("correlation_matrix.png", dpi=120)
Example — Multicollinearity Test (VIF):
from statsmodels.stats.outliers_influence import variance_inflation_factor
X = df[numeric_cols].dropna()
vif_data = pd.DataFrame({
"feature": X.columns,
"VIF": [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
})
print(vif_data)
# VIF > 10 → high multicollinearity → consider removing a variable
Key principle: The more the AI model knows about your dataset (via the EDA report), the more precise and tailored the generated code will be to your actual data.
Module 2 — Using AI Tools for Analyzing Your Data
Module Overview
graph LR
subgraph "Source Data"
A["BLS QCEW 2024\n4 quarters"]
B["BLS QCEW Q1 2025\n1 quarter"]
end
subgraph "Analyses"
C["Time Series\nTemporal trends"]
D["Cohort Analysis\nAt-Risk vs Non-Risk"]
E["Hypothesis Testing\nt-test, ANOVA,\nMann-Whitney U"]
end
subgraph "Results"
F["Quarterly\nvisualizations"]
G[Exported CSV files]
H[Statistical insights]
end
A --> C
B --> C
C --> D
D --> E
C --> F
D --> G
E --> H
Working with Time Series Data
Difference Between Annual and Quarterly Data
| Type | Frequency | Data Points | Usage |
|---|---|---|---|
Annual (annual) | 1 per year | 1 point per sector | Overview |
Quarterly (qtr) | 4 per year | 4-5 points per sector | Trend analysis |
To detect the impact of AI on employment, quarterly data is preferred because it allows observing changes from one period to the next.
Analysis Prompt for Claude
Uploaded files:
1. eda_quarterly_sample.md → EDA report on the quarterly file
2. field_layouts.csv → Column schema
3. ai_vulnerable_industries.md → 47 vulnerable sectors (3 excluded
because their codes are number ranges)
Prompt:
"Generate Python code for a Jupyter Notebook that:
- Loads BLS quarterly data for 2024 and Q1 2025
- Identifies significant trends over time
- Focuses on AI-vulnerable sectors
- Analyzes impact on employment rates AND wages
- Generates visualizations of quarterly changes
- Exports results to CSV"
Code — Time Series Analysis (TimeSeries.ipynb)
# TimeSeries.ipynb — BLS Time Series Analysis
# Generated by Claude from EDA report and at-risk sector list
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
import warnings
warnings.filterwarnings('ignore')
plt.rcParams['figure.dpi'] = 120
plt.rcParams['figure.figsize'] = (14, 6)
# ─── 1. Load data ─────────────────────────────────────────────────────────────
dtype_hints = {
"area_fips": str, "industry_code": str, "qtr": str,
"disclosure_code": str, "own_code": "Int64",
"agglvl_code": "Int64", "year": "Int64",
}
df_2024 = pd.read_csv("2024.q1-q4.singlefile.csv",
dtype=dtype_hints, low_memory=False)
df_2025 = pd.read_csv("2025.q1.singlefile.csv",
dtype=dtype_hints, low_memory=False)
df = pd.concat([df_2024, df_2025], ignore_index=True)
print(f"Total rows: {len(df):,}")
# ─── 2. Filters: national level, private ownership, quarters only ─────────────
df = df[
(df['area_fips'] == 'US000') & # National only
(df['own_code'] == 5) & # Private sector
(df['agglvl_code'] == 74) & # Detailed industry level
(df['qtr'].isin(['1', '2', '3', '4'])) # Quarters only (not 'A')
]
# ─── 3. Load AI-vulnerable sectors ───────────────────────────────────────────
risk_df = pd.read_csv("industry-risk.csv", dtype=str)
risk_codes = set(risk_df['industry_code'].str.strip())
df['at_risk'] = df['industry_code'].isin(risk_codes)
# ─── 4. Convert numeric columns ──────────────────────────────────────────────
numeric_cols = ['annual_avg_emplvl', 'total_annual_wages',
'annual_avg_wkly_wage', 'avg_annual_pay', 'annual_avg_estabs']
for col in numeric_cols:
df[col] = pd.to_numeric(df[col], errors='coerce')
# Mask rows with restricted disclosure
df.loc[df['disclosure_code'] == 'N', numeric_cols] = pd.NA
# ─── 5. Create period column (YYYY-QX) ────────────────────────────────────────
df['period'] = df['year'].astype(str) + '-Q' + df['qtr'].astype(str)
period_order = sorted(df['period'].unique())
df['period_cat'] = pd.Categorical(df['period'], categories=period_order, ordered=True)
# ─── 6. Aggregate by period ──────────────────────────────────────────────────
agg = df.groupby(['period_cat', 'at_risk']).agg(
total_estabs = ('annual_avg_estabs', 'sum'),
total_employ = ('annual_avg_emplvl', 'sum'),
total_wages = ('total_annual_wages', 'sum'),
mean_pay = ('avg_annual_pay', 'mean'),
median_pay = ('avg_annual_pay', 'median'),
).reset_index()
# ─── 7. Calculate quarterly changes ──────────────────────────────────────────
for group_val, label in [(True, 'at_risk'), (False, 'non_risk')]:
subset = agg[agg['at_risk'] == group_val].sort_values('period_cat')
for col in ['total_employ', 'total_wages', 'mean_pay']:
pct_col = f'{col}_pct_chg_{label}'
agg.loc[agg['at_risk'] == group_val, pct_col] = (
subset[col].pct_change() * 100
).values
print(agg[['period_cat', 'at_risk', 'total_employ', 'mean_pay']].to_string())
# ─── 8. Visualizations ───────────────────────────────────────────────────────
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
for ax, (group_val, label, color) in zip(
axes.flat[:2],
[(True, 'AI-Risk Sectors', 'crimson'),
(False, 'Non-Risk Sectors', 'steelblue')]
):
subset = agg[agg['at_risk'] == group_val].sort_values('period_cat')
ax.bar(subset['period_cat'].astype(str), subset['total_employ'],
color=color, alpha=0.8)
ax.set_title(f"Total Employment — {label}")
ax.set_xlabel("Quarter")
ax.set_ylabel("Total Average Employment")
ax.tick_params(axis='x', rotation=45)
for ax, (group_val, label, color) in zip(
axes.flat[2:],
[(True, 'AI-Risk Sectors', 'crimson'),
(False, 'Non-Risk Sectors', 'steelblue')]
):
subset = agg[agg['at_risk'] == group_val].sort_values('period_cat')
ax.plot(subset['period_cat'].astype(str), subset['mean_pay'],
marker='o', color=color, linewidth=2)
ax.set_title(f"Average Annual Pay — {label}")
ax.set_xlabel("Quarter")
ax.set_ylabel("Average Pay ($)")
ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda x, _: f'${x:,.0f}'))
ax.tick_params(axis='x', rotation=45)
plt.suptitle("AI Impact on Employment and Wages — BLS QCEW", fontsize=14)
plt.tight_layout()
plt.savefig("time_series_analysis.png", dpi=120)
plt.show()
# ─── 9. Export results ───────────────────────────────────────────────────────
agg.to_csv("time_series_results.csv", index=False)
print("File exported: time_series_results.csv")
Observed Results
Raw results — Aggregation by quarter (at-risk sectors):
┌──────────────┬─────────────┬─────────────┬──────────────┐
│ Period │ Establishments│ Total Employ│ Average Pay │
├──────────────┼─────────────┼─────────────┼──────────────┤
│ 2024-Q1 │ stable │ stable │ stable │
│ 2024-Q2 │ stable │ stable │ stable │
│ 2024-Q3 │ stable │ stable │ stable │
│ 2024-Q4 │ stable │ stable │ stable │
│ 2025-Q1 │ ↓ decline │ ↓ decline │ ↓ decline │
└──────────────┴─────────────┴─────────────┴──────────────┘
Note: The decline in 2025-Q1 is significant but not conclusive.
It could be due to factors unrelated to AI.
Important lesson: Before drawing conclusions, much more historical data and rigorous statistical analysis are needed to prove a causal relationship.
Cohort Analysis
Objective of Cohort Analysis
The limitation of the previous time series analysis is the absence of statistical context. Cohort analysis compares trends in AI-vulnerable sectors against all other sectors, allowing us to determine whether their experience is genuinely different.
Follow-up Prompt to Claude
"The previous code works correctly, no need to revise it.
Now, generate additional code for a cohort analysis
that compares the performance trends of AI-vulnerable
industries (At-Risk) with all other industries (Non-Risk).
Include comparative statistics and visualizations."
Code — Cohort Analysis (analysis_pipeline.py)
# analysis_pipeline.py — Cohort Analysis
# Generated by Claude
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
from scipy import stats
# ─── Create cohorts ──────────────────────────────────────────────────────────
risk_codes = set(risk_df['industry_code'].str.strip())
df['cohort'] = df['industry_code'].apply(
lambda x: 'At-Risk' if x in risk_codes else 'Non-Risk'
)
print("Cohort distribution:")
print(df['cohort'].value_counts())
# Check for overlap between cohorts
overlap = risk_codes.intersection(set(df[df['cohort'] == 'Non-Risk']['industry_code']))
if overlap:
print(f"\nWarning: {len(overlap)} overlapping codes detected")
# ─── Aggregate by cohort and quarter ─────────────────────────────────────────
cohort_agg = df.groupby(['period_cat', 'cohort']).agg(
n_industries = ('industry_code', 'nunique'),
total_employ = ('annual_avg_emplvl', 'sum'),
total_wages = ('total_annual_wages', 'sum'),
mean_pay = ('avg_annual_pay', 'mean'),
median_pay = ('avg_annual_pay', 'median'),
).reset_index()
# ─── Calculate relative performance ratio ────────────────────────────────────
pivot_employ = cohort_agg.pivot(
index='period_cat', columns='cohort', values='total_employ'
)
pivot_employ['performance_ratio'] = (
pivot_employ['At-Risk'] / pivot_employ['Non-Risk']
)
print("\nAt-Risk / Non-Risk performance ratio:")
print(pivot_employ[['At-Risk', 'Non-Risk', 'performance_ratio']].to_string())
# ─── Calculate overall averages by cohort ────────────────────────────────────
overall = df.groupby('cohort').agg(
mean_employ = ('annual_avg_emplvl', 'mean'),
mean_pay = ('avg_annual_pay', 'mean'),
median_pay = ('avg_annual_pay', 'median'),
).reset_index()
print("\nOverall averages by cohort:")
print(overall.to_string())
# ─── Visualization: comparative wage trends ──────────────────────────────────
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
for cohort, color in [('At-Risk', 'crimson'), ('Non-Risk', 'steelblue')]:
subset = cohort_agg[cohort_agg['cohort'] == cohort].sort_values('period_cat')
ax1.plot(subset['period_cat'].astype(str), subset['mean_pay'],
marker='o', label=cohort, color=color, linewidth=2)
ax1.set_title("Average Pay — At-Risk vs Non-Risk Comparison")
ax1.set_xlabel("Quarter")
ax1.set_ylabel("Average Annual Pay ($)")
ax1.yaxis.set_major_formatter(mtick.FuncFormatter(lambda x, _: f'${x:,.0f}'))
ax1.legend()
ax1.tick_params(axis='x', rotation=45)
# Performance ratio
pivot_employ['performance_ratio'].plot(
kind='bar', ax=ax2, color='purple', alpha=0.7
)
ax2.axhline(y=1.0, color='black', linestyle='--', label='Parity')
ax2.set_title("Performance Ratio: At-Risk / Non-Risk (Total Employment)")
ax2.set_xlabel("Quarter")
ax2.set_ylabel("Ratio")
ax2.legend()
ax2.tick_params(axis='x', rotation=45)
plt.tight_layout()
plt.savefig("cohort_analysis.png", dpi=120)
plt.show()
# ─── Export ──────────────────────────────────────────────────────────────────
cohort_agg.to_csv("cohort_analysis_results.csv", index=False)
print("Exported: cohort_analysis_results.csv")
Cohort Analysis Results
Cohorts created:
• At-Risk : 42 industrial sectors (note: 47 expected → probable duplicates)
• Non-Risk : X remaining industrial sectors
Observations:
• Wages are globally HIGHER in At-Risk sectors
(tech, finance, legal sectors → naturally higher salaries)
• The decline in 2025-Q1 is visible in BOTH cohorts
• The performance ratio suggests increased vulnerability for At-Risk
at the wage level
Hypothesis Testing Tools
AI’s Role in Choosing Statistical Tests
AI helps to:
- Select the right test based on data characteristics
- Generate implementation code
- Interpret results with natural language explanations
Common Python Imports for Statistical Tests
These imports are directly taken from Module 2 slides:
import numpy as np
import pandas as pd
from scipy import stats
import statsmodels.api as sm
from statsmodels.formula.api import ols
from statsmodels.stats.multicomp import pairwise_tukeyhsd
Statistical Test Selection Guide
graph TD
Q{What type\nof data?} --> N[Continuous numerical]
Q --> C[Categorical]
N --> G{Normal\ndistribution?}
G -->|Yes, 2 groups| T["t-test\nStudent's t"]
G -->|Yes, 3+ groups| A[ANOVA]
G -->|No, 2 groups| MW[Mann-Whitney U]
G -->|No, 3+ groups| KW[Kruskal-Wallis]
C --> CH["Chi-square\nTest"]
T --> E1["Compare means\nof 2 groups\nEx: response to 2 drugs"]
A --> E2["Compare means\nof 3+ groups\nWith post-hoc Tukey HSD"]
MW --> E3["Compare distributions\nwhen data is non-normal"]
CH --> E4["Association between\n2 categorical variables\nEx: smoking and lung cancer"]
Code Examples by Test
t-test (comparing 2 groups):
# Compare average wages: At-Risk vs Non-Risk
at_risk_pay = df[df['cohort'] == 'At-Risk']['avg_annual_pay'].dropna()
non_risk_pay = df[df['cohort'] == 'Non-Risk']['avg_annual_pay'].dropna()
t_stat, p_value = stats.ttest_ind(at_risk_pay, non_risk_pay)
print(f"t-test: t = {t_stat:.4f}, p = {p_value:.4e}")
print(f"{'Significant' if p_value < 0.05 else 'Non-significant'} difference (α = 0.05)")
ANOVA (comparing 3+ groups):
# Compare wages by quarter
groups = [
df[df['period'] == period]['avg_annual_pay'].dropna()
for period in sorted(df['period'].unique())
]
f_stat, p_value = stats.f_oneway(*groups)
print(f"ANOVA: F = {f_stat:.4f}, p = {p_value:.4e}")
# Post-hoc Tukey HSD test
tukey = pairwise_tukeyhsd(
endog=df['avg_annual_pay'].dropna(),
groups=df.loc[df['avg_annual_pay'].notna(), 'period'],
alpha=0.05
)
print(tukey)
Chi-square (categorical variables):
# Test the association between cohort and disclosure code
contingency = pd.crosstab(df['cohort'], df['disclosure_code'])
chi2, p, dof, expected = stats.chi2_contingency(contingency)
print(f"Chi-square: χ² = {chi2:.4f}, p = {p:.4e}, df = {dof}")
Mann-Whitney U (non-parametric):
# For non-normal data (like avg_annual_pay)
stat, p_value = stats.mannwhitneyu(at_risk_pay, non_risk_pay, alternative='two-sided')
print(f"Mann-Whitney U: U = {stat:.0f}, p = {p_value:.4e}")
Statistical Test Summary Table
| Test | Conditions | Use Case | Python |
|---|---|---|---|
| t-test | Normal, 2 groups, continuous variable | Compare wages of 2 sectors | scipy.stats.ttest_ind |
| ANOVA | Normal, 3+ groups, continuous variable | Compare wages across multiple sectors | scipy.stats.f_oneway |
| Chi-square | Categorical variables | Smoking/cancer association | scipy.stats.chi2_contingency |
| Mann-Whitney U | Non-normal, 2 groups | Comparison of skewed distributions | scipy.stats.mannwhitneyu |
| Kruskal-Wallis | Non-normal, 3+ groups | Non-parametric alternative to ANOVA | scipy.stats.kruskal |
Module 3 — Using AI Tools for Generating Analytical Reports
Module Overview
sequenceDiagram
participant Analyst
participant Claude
participant Grok
participant ChatGPT
Analyst->>Claude: Cohort analysis data + report prompt
Claude->>Analyst: Python code for PDF report generation
Analyst->>Analyst: Execute code → PDF report
Analyst->>ChatGPT: "Check this report for accuracy"
ChatGPT->>Analyst: ⚠️ Issues detected!\n(implausible 2025-Q1 decline)
Analyst->>ChatGPT: "Generate code to test the data"
ChatGPT->>Analyst: Data verification code
Analyst->>Analyst: Identify the bug in Claude's code
Analyst->>Claude: Fix the 2025-Q1 processing
Analyst->>ChatGPT: "Generate parameterized template + Git"
ChatGPT->>Analyst: demos.ipynb — automated template
Designing AI-Generated Reports
Demo Context
This section takes the results from the cohort analysis in Module 2 and uses them to generate a professional PDF report.
The input data is the comparative analysis results:
| Metric | At-Risk | Non-Risk |
|---|---|---|
| Number of industries | ~42 | Remainder |
| Average wage | Higher | Lower |
| Employment trend 2024-Q1 → 2025-Q1 | -15 to -17% | -15 to -17% |
| Wage performance ratio | Increased vulnerability | Reference |
⚠️ Detected anomaly: The 15-17% drop in total employment between Q1 2024 and Q1 2025 is suspicious. It will be identified as a code bug later.
Report Generation Prompt
"Generate Python code to create a professional PDF report
from the results of this cohort analysis.
The report should include:
- Executive summary
- Methodology
- Results by cohort (At-Risk vs Non-Risk)
- Embedded visualizations
- Conclusions and recommendations
Available data:
[Paste the numerical results of the cohort analysis]"
Code — PDF Report Generation
# Report generation with ReportLab or WeasyPrint
# (alternative approach with Markdown → PDF)
import pandas as pd
from datetime import datetime
import subprocess
def generate_markdown_report(cohort_results: pd.DataFrame,
output_file: str = "report.md") -> str:
"""Generates a Markdown report from analysis results."""
today = datetime.now().strftime("%B %d, %Y")
report = f"""# Analysis Report — AI Impact on Employment
## Bureau of Labor Statistics — QCEW
**Date:** {today}
**Author:** Automated Analysis
---
## 1. Executive Summary
This analysis compares employment and wage trends between industrial sectors
identified as vulnerable to AI adoption (At-Risk cohort)
and other sectors (Non-Risk cohort), using quarterly data from
the Bureau of Labor Statistics.
## 2. Methodology
- **Source:** BLS QCEW (Quarterly Census of Employment and Wages)
- **Period:** Q1 2024 — Q1 2025 (5 quarters)
- **At-risk sectors:** 47 sectors identified by AI analysis (Grok)
- **Analysis level:** National (US000), private sector (own_code = 5)
## 3. Results
### 3.1 Cohort Distribution
| Cohort | Number of Industries |
|---------|-------------------|
| At-Risk | 42 |
| Non-Risk | X |
### 3.2 Performance Comparison
{cohort_results.to_markdown(index=False)}
## 4. Conclusions
Results suggest [insert interpretation based on actual data].
## 5. Recommendations
1. Expand the analysis to more historical years
2. Integrate data on new hires vs. layoffs
3. Compare with AI investment by companies per sector
"""
with open(output_file, 'w', encoding='utf-8') as f:
f.write(report)
return output_file
# Convert Markdown to PDF (requires pandoc)
def convert_to_pdf(md_file: str, pdf_file: str):
subprocess.run(
["pandoc", md_file, "-o", pdf_file, "--pdf-engine=wkhtmltopdf"],
check=True
)
print(f"PDF report generated: {pdf_file}")
Verifying AI Reports
Why Verify AI-Generated Reports?
An AI-generated report may look perfect while containing subtle errors in data or conclusions. Cross-verification by a second AI model is a recommended practice.
flowchart LR
A["Report generated\nby Claude + Grok"] --> B["Submit to\nChatGPT for review"]
B --> C{ChatGPT\nfinds issues?}
C -->|✅ No| D["Report validated\npublication"]
C -->|❌ Yes| E["Identify the issue\nwith verification code"]
E --> F["Fix the original\ncode"]
F --> A
The Bug Discovered by ChatGPT
Identified problem: The 15-17% decline in 2025-Q1 is implausible in a normal economic context.
ChatGPT:
"I see several important issues with this report:
1. ❌ The ~15% employment decline Q1-2024 → Q1-2025 is implausible.
Such a drop suggests a problem in the data or the code.
2. ❌ Total wages fell by the same magnitude over the same period —
this is not consistent with real economic data.
3. ❌ Average employment numbers do not match cumulative totals.
The 'Q1 vs Q1' comparison is incorrectly calculated.
4. ⚠️ Multi-level NAICS industry codes can create double-counting —
verify that agglvl_code is filtering correctly."
Data Verification Code
# Code generated by ChatGPT to diagnose the issue
def verify_data_integrity(df_2024, df_2025):
"""Data consistency checks."""
print("=== Data Integrity Verification ===\n")
# 1. Check row counts
print(f"Rows 2024: {len(df_2024):,}")
print(f"Rows 2025: {len(df_2025):,}")
print(f"Ratio: {len(df_2025)/len(df_2024):.2f}")
# 2. Check available quarters
print(f"\nQuarters 2024: {sorted(df_2024['qtr'].unique())}")
print(f"Quarters 2025: {sorted(df_2025['qtr'].unique())}")
# 3. BUG DETECTED: the 2025 file only contains Q1
# If we compare "total 2024" to "Q1 2025", we compare 4 quarters to 1!
q1_2024 = df_2024[df_2024['qtr'] == '1']['annual_avg_emplvl'].sum()
q1_2025 = df_2025[df_2025['qtr'] == '1']['annual_avg_emplvl'].sum()
print(f"\nTotal employment Q1 2024: {q1_2024:,.0f}")
print(f"Total employment Q1 2025: {q1_2025:,.0f}")
print(f"Actual Q1/Q1 variation: {(q1_2025 - q1_2024) / q1_2024 * 100:.1f}%")
verify_data_integrity(df_2024, df_2025)
The Bug and Its Fix
ROOT CAUSE:
Claude's code treated the 2025-Q1 file as if it contained
a full year, summing all quarters.
The 2025 file only contains Q1 (incomplete data since the year
is not over). Comparing the sum of 4 quarters of 2024
to 1 quarter of 2025 = artificial decline of ~75%.
FIX:
ALWAYS compare the same quarter (Q1 vs Q1),
not annual aggregates with incomplete files.
# FIX: Compare only Q1 vs Q1
def compare_same_quarter(df_2024, df_2025, quarter='1'):
"""Correct comparison: same quarter, different years."""
q_2024 = df_2024[
(df_2024['qtr'] == quarter) &
(df_2024['area_fips'] == 'US000') &
(df_2024['own_code'] == 5) &
(df_2024['agglvl_code'] == 74)
].copy()
q_2025 = df_2025[
(df_2025['qtr'] == quarter) &
(df_2025['area_fips'] == 'US000') &
(df_2025['own_code'] == 5) &
(df_2025['agglvl_code'] == 74)
].copy()
# Merge on industry_code for direct comparison
merged = q_2024.merge(
q_2025[['industry_code', 'annual_avg_emplvl', 'avg_annual_pay']],
on='industry_code',
suffixes=('_2024', '_2025')
)
merged['employ_chg_pct'] = (
(merged['annual_avg_emplvl_2025'] - merged['annual_avg_emplvl_2024'])
/ merged['annual_avg_emplvl_2024'] * 100
)
merged['pay_chg_pct'] = (
(merged['avg_annual_pay_2025'] - merged['avg_annual_pay_2024'])
/ merged['avg_annual_pay_2024'] * 100
)
return merged
comparison = compare_same_quarter(df_2024, df_2025, quarter='1')
print("Q1 2024 → Q1 2025 variation:")
print(comparison[['industry_code', 'employ_chg_pct', 'pay_chg_pct']].describe())
Lesson: 3-Step Verification Strategy
1. AUTOMATED VERIFICATION
→ Use a second AI model to review the report
2. DATA VERIFICATION
→ Generate validation code (sanity checks)
→ Compare totals with known external sources
3. BUSINESS LOGIC VERIFICATION
→ Do the trends make economic sense?
→ Are drastic changes explainable?
Automating Report Generation
Why Automate?
Reports almost never serve only once. As soon as data is updated (new quarter, new year), everything must be re-run. Automation allows you to:
- Simply replace the source data file
- Automatically incorporate historical results
- Maintain traceability via Git
- Reduce human errors
Parameterized Template (demos.ipynb)
The following notebook is the complete automation template generated by ChatGPT. Simply modify the parameters at the top to re-run a complete analysis on a new dataset:
Cell 1 — Description
# Employment & Wages Analysis — Parameterized Template
This notebook:
- Loads the latest version of the data file
- Runs a standard cleaning and summary pipeline
- Saves artifacts and a JSON summary for future comparisons
- Compares current metrics with the last previous run
- Commits artifacts to Git for versioned tracking
Cell 2 — Parameters (only cell to modify)
# PARAMETERS
from pathlib import Path
from datetime import datetime
DATA_DIR = Path("/mnt/data") # Directory containing CSVs
FILE_PATTERN = "employment_*.csv" # File naming pattern
ARTIFACTS_DIR = Path("./artifacts")
ARTIFACTS_DIR.mkdir(exist_ok=True, parents=True)
RUN_SUMMARIES_DIR = ARTIFACTS_DIR / "run_summaries"
RUN_SUMMARIES_DIR.mkdir(exist_ok=True, parents=True)
GIT_REPO_ROOT = Path(".").resolve() # Git repository root
DISPLAY_TOP_N = 10 # Number of results to display
DONT_COMMIT_TO_GIT = False # Set True to disable commit
Cell 3 — Imports
# Imports
import re, json, glob, subprocess, getpass, math
from typing import Dict, Any, Optional
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.rcParams["figure.dpi"] = 120
Cell 4 — Utility Functions
# Utilities
def find_latest_file(directory: Path, pattern: str):
"""Automatically finds the most recent data file."""
files = sorted(
directory.glob(pattern),
key=lambda p: p.stat().st_mtime,
reverse=True
)
return files[0] if files else None
def safe_read_csv(path: Path, nrows: Optional[int] = None,
dtype_hints: dict = None):
"""Robust CSV reading with encoding fallback."""
if dtype_hints is None:
dtype_hints = {}
try:
return pd.read_csv(path, dtype=dtype_hints,
low_memory=False, nrows=nrows)
except UnicodeDecodeError:
return pd.read_csv(path, dtype=dtype_hints,
encoding="latin1", low_memory=False, nrows=nrows)
Cell 5 — Loading the Most Recent File
# Load latest
latest = find_latest_file(Path(DATA_DIR), FILE_PATTERN)
if latest is None:
# Fallback: take any CSV in the directory
candidates = sorted(
Path(DATA_DIR).glob("*.csv"),
key=lambda p: p.stat().st_mtime,
reverse=True
)
latest = candidates[0] if candidates else None
if latest is None:
raise FileNotFoundError("No data file found.")
print("Loading file:", latest)
dtype_hints = {
"area_fips": str, "industry_code": str, "qtr": str,
"disclosure_code": str,
"lq_disclosure_code": str, "oty_disclosure_code": str,
"own_code": "Int64", "agglvl_code": "Int64",
"size_code": "Int64", "year": "Int64",
}
df = safe_read_csv(latest, dtype_hints=dtype_hints)
df.head()
Cell 6 — Data Cleaning
# Basic cleaning
def basic_cleaning(df):
"""Standardized BLS QCEW dataset cleaning."""
df = df.copy()
df.columns = [c.strip() for c in df.columns]
if "year" in df.columns:
df['year'] = pd.to_numeric(df['year'], errors='coerce').astype('Int64')
# Normalize empty disclosure codes
for col in ['disclosure_code', 'lq_disclosure_code', 'oty_disclosure_code']:
if col in df.columns:
df[col] = df[col].replace({"": pd.NA})
# Convert key numeric columns
numeric_candidates = [
'annual_avg_estabs', 'annual_avg_emplvl', 'total_annual_wages',
'annual_avg_wkly_wage', 'avg_annual_pay'
]
for c in numeric_candidates:
if c in df.columns:
df[c] = pd.to_numeric(df[c], errors='coerce')
# Mask values suppressed for privacy
if 'disclosure_code' in df.columns:
mask = df['disclosure_code'] == 'N'
for c in numeric_candidates:
if c in df.columns:
df.loc[mask, c] = pd.NA
# Clean whitespace in text columns
for c in df.select_dtypes(include=['object']).columns:
df[c] = df[c].astype(str).str.strip()
return df
df = basic_cleaning(df)
df.info()
Cell 7 — Computing the Run Summary
# Run summary
def compute_run_summary(df):
"""Calculates key metrics for the current run."""
summary = {}
summary['rows'] = int(len(df))
summary['unique_areas'] = (
df['area_fips'].nunique() if 'area_fips' in df.columns else None
)
summary['unique_industries'] = (
df['industry_code'].nunique() if 'industry_code' in df.columns else None
)
summary['total_annual_emplvl_sum'] = (
float(df['annual_avg_emplvl'].sum(min_count=1))
if 'annual_avg_emplvl' in df.columns else None
)
summary['total_annual_wages_sum'] = (
float(df['total_annual_wages'].sum(min_count=1))
if 'total_annual_wages' in df.columns else None
)
summary['mean_avg_annual_pay'] = (
float(df['avg_annual_pay'].mean(skipna=True))
if 'avg_annual_pay' in df.columns else None
)
summary['median_avg_annual_pay'] = (
float(df['avg_annual_pay'].median(skipna=True))
if 'avg_annual_pay' in df.columns else None
)
summary['run_ts'] = datetime.utcnow().isoformat() + "Z"
return summary
current_summary = compute_run_summary(df)
current_summary
Cell 8 — Saving and Version Control
# Save summary + Git commit
def save_run_summary(summary, dir, label=None):
"""Saves the summary as JSON (recommended format for comparisons)."""
label = label or datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
path = dir / f"summary_{label}.json"
with open(path, "w") as f:
json.dump(summary, f, indent=2)
return path
run_label = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
summary_path = save_run_summary(current_summary, RUN_SUMMARIES_DIR,
label=run_label)
print(f"Summary saved: {summary_path}")
# Compare with previous run
previous_summaries = sorted(RUN_SUMMARIES_DIR.glob("summary_*.json"))
if len(previous_summaries) >= 2:
prev_path = previous_summaries[-2] # second to last
with open(prev_path) as f:
prev_summary = json.load(f)
print("\n=== Comparison with previous run ===")
for key in ['total_annual_emplvl_sum', 'mean_avg_annual_pay',
'median_avg_annual_pay']:
if key in current_summary and key in prev_summary:
curr_val = current_summary[key]
prev_val = prev_summary[key]
if prev_val and prev_val != 0:
pct_chg = (curr_val - prev_val) / abs(prev_val) * 100
print(f" {key}: {prev_val:,.0f} → {curr_val:,.0f} "
f"({pct_chg:+.1f}%)")
# Automatic Git commit (if enabled)
if not DONT_COMMIT_TO_GIT:
try:
subprocess.run(
["git", "add", str(ARTIFACTS_DIR)],
cwd=GIT_REPO_ROOT, check=True
)
subprocess.run(
["git", "commit", "-m",
f"Auto-analysis run {run_label}"],
cwd=GIT_REPO_ROOT, check=True
)
print(f"\nGit commit made: Auto-analysis run {run_label}")
except subprocess.CalledProcessError as e:
print(f"Git commit failed (non-blocking): {e}")
Generated Artifact Structure
artifacts/
├── run_summaries/
│ ├── summary_20240115T093000Z.json ← Run 1
│ ├── summary_20240415T110000Z.json ← Run 2
│ └── summary_20240715T143000Z.json ← Run 3 (current)
├── time_series_results.csv
├── cohort_analysis_results.csv
├── distribution_analysis.png
├── time_series_analysis.png
└── cohort_analysis.png
Example summary_YYYYMMDD.json File
{
"rows": 36145289,
"unique_areas": 4024,
"unique_industries": 1287,
"total_annual_emplvl_sum": 142857000000.0,
"total_annual_wages_sum": 8943210000000.0,
"mean_avg_annual_pay": 68420.5,
"median_avg_annual_pay": 52100.0,
"run_ts": "2025-04-15T11:00:00Z"
}
Why JSON for summaries? The JSON format is readable by both machines AND humans. Future runs can directly load these files to compare historical trends without re-reading the massive source CSVs.
Key Concepts and Best Practices
Golden Rules for Data Analysis with AI
mindmap
root((Best Practices))
Prepare context
EDA report before any analysis prompt
Documented column schema
List of known constraints
Prompt sizing
Large CSV → 1,000-row sample
Use small text files as context
Combine multiple small files rather than one large one
Verification
Always review code before running
Cross-check by a second AI model
Sanity checks on extreme values
Iteration
Don't aim for perfection on the first prompt
Follow-up prompts to refine
Switch models if quota is reached
Automation
Parameterized template for reproducibility
Git for traceability
JSON for historical comparisons
Choosing the Right AI Model for Each Task
| Task | Recommended Model | Justification |
|---|---|---|
| Dataset suggestions | ChatGPT | Good knowledge of official sources |
| Python code generation | Claude | High-quality, well-commented code |
| Real-world awareness / trend identification | Grok | Strong integration of current information |
| Verification and fact-checking | ChatGPT | Neutral third party for validation |
| General tasks | Vary models | Complementary strengths |
Complete Summary Pipeline
Step 1: Identify data source (ChatGPT)
↓
Step 2: Download and understand the schema (BLS.gov)
↓
Step 3: Create a 1,000-row sample
↓
Step 4: Generate EDA report (OpenAI API via eda_pipeline.py)
↓
Step 5: Identify at-risk sectors (Grok + upload NAICS codes CSV)
↓
Step 6: Generate analysis code (Claude + 3 context files)
↓
Step 7: Execute and verify results
↓
Step 8: Generate PDF report (Grok)
↓
Step 9: Fact-check the report (ChatGPT)
↓
Step 10: Fix identified errors
↓
Step 11: Automate with parameterized template (ChatGPT + Git)
Tools and Resources
Data Sources
| Resource | URL | Description |
|---|
Python Libraries Used
# Data manipulation
import pandas as pd # DataFrames, CSV reading
import numpy as np # Numerical computations
# Visualization
import matplotlib.pyplot as plt # Basic charts
import seaborn as sns # Statistical visualizations
# Statistics
from scipy import stats # Statistical tests (t-test, chi², etc.)
import statsmodels.api as sm # Advanced statistical models
from statsmodels.formula.api import ols # Linear regression (ANOVA)
from statsmodels.stats.multicomp import pairwise_tukeyhsd # Post-hoc ANOVA
# AI / LLM
from openai import OpenAI # OpenAI API (ChatGPT, GPT-4o)
# Utilities
from dotenv import load_dotenv # Credentials management
from pathlib import Path # File path management
import json # Summary serialization
import subprocess # Automated Git calls
AI Models Used in This Course
ChatGPT (OpenAI) → EDA, verification, automation
Claude (Anthropic) → Python code generation
Grok (xAI) → Trend identification, report generation
Course File Structure
generative-ai-data-analysis/
├── 01/ ← Module 1
│ └── demos/
│ ├── eda_pipeline.py ← EDA via OpenAI API
│ ├── analysis_pipeline.py ← Cohort analysis
│ └── dist_analysis_gpt.py ← Distribution analysis
├── 02/ ← Module 2
│ └── demos/
│ ├── TimeSeries.ipynb ← Time series analysis
│ ├── forecast.py ← Forecasting
│ └── industry-risk.csv ← 47 AI-risk sectors
└── 03/ ← Module 3
└── demos.ipynb ← Complete parameterized template
Search Terms
data · analysis · generative · ai · llm · application · development · artificial · intelligence · report · tools · cohort · prompt · reports · statistical · eda · generation · test · ai-generated · bug · choosing · claude · context · dataset