Intermediate

Data Analysis with Generative AI

Use AI tools for exploratory data analysis, analysis and generating polished analytical reports.

Table of Contents

  1. Module 1 — Using AI Tools for Exploratory Data Analysis
  2. Module 2 — Using AI Tools for Analyzing Your Data
  3. Module 3 — Using AI Tools for Generating Analytical Reports
  4. Overall Workflow
  5. Key Concepts and Best Practices
  6. 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

CharacteristicValue
Number of rows~36 million
Number of columns38
FormatCSV
FrequencyQuarterly / Annual
CoverageUnited 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

TypeFrequencyData PointsUsage
Annual (annual)1 per year1 point per sectorOverview
Quarterly (qtr)4 per year4-5 points per sectorTrend 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:

  1. Select the right test based on data characteristics
  2. Generate implementation code
  3. 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

TestConditionsUse CasePython
t-testNormal, 2 groups, continuous variableCompare wages of 2 sectorsscipy.stats.ttest_ind
ANOVANormal, 3+ groups, continuous variableCompare wages across multiple sectorsscipy.stats.f_oneway
Chi-squareCategorical variablesSmoking/cancer associationscipy.stats.chi2_contingency
Mann-Whitney UNon-normal, 2 groupsComparison of skewed distributionsscipy.stats.mannwhitneyu
Kruskal-WallisNon-normal, 3+ groupsNon-parametric alternative to ANOVAscipy.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:

MetricAt-RiskNon-Risk
Number of industries~42Remainder
Average wageHigherLower
Employment trend 2024-Q1 → 2025-Q1-15 to -17%-15 to -17%
Wage performance ratioIncreased vulnerabilityReference

⚠️ 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

TaskRecommended ModelJustification
Dataset suggestionsChatGPTGood knowledge of official sources
Python code generationClaudeHigh-quality, well-commented code
Real-world awareness / trend identificationGrokStrong integration of current information
Verification and fact-checkingChatGPTNeutral third party for validation
General tasksVary modelsComplementary 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

ResourceURLDescription

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

Interested in this course?

Contact us to book it or get a custom training plan for your team.