Featured image of post Building an Automated Market Research Report Generator with DuckDB

Building an Automated Market Research Report Generator with DuckDB

Learn to build an automated market research report generator using DuckDB. From raw data to professional Markdown/PDF reports — a complete guide with Python and SQL code examples.

Building an Automated Market Research Report Generator with DuckDB

Have you noticed a lucrative opportunity? Many small e-commerce sellers, independent brands, and even traditional enterprises spend thousands of yuan every month on market research. But they don’t need McKinsey-level deep analysis — they need fast, accurate, and cost-effective market trend analysis.

This is your chance.

Today I’ll teach you how to build an “Automated Market Research Report Generator” using DuckDB. You just need to provide raw data (from public APIs, web scraping, or user-uploaded CSVs), and DuckDB will automatically handle data cleaning, trend analysis, competitor comparison, and finally generate a professional Markdown/PDF report that’s ready to sell.

This approach has been proven — developers have built “cross-border e-commerce product selection reports” as a service, selling each report for 99-499 yuan and earning tens of thousands monthly.

Why Choose DuckDB?

The traditional market research workflow is: collect data → import into Excel/Python → manual analysis → write report. This process takes days and is error-prone.

With DuckDB, the approach is completely different:

  1. Data sources (CSV/JSON/API) → read directly in one line
  2. All analysis (trends, comparisons, statistics) → pure SQL queries
  3. Report generation → SQL results concatenated into Markdown

The entire process can be completed in 5 minutes, and it’s fully automated every time. Compared to traditional approaches, efficiency improves by more than 10x.

DimensionTraditional Excel/ManualPandas + JupyterDuckDB
Data processing speedSlow (single-threaded)MediumVery fast (vectorized)
Memory usageHigh (Excel limits)Medium (RAM-limited)Low (streaming)
Learning curveLowMediumLow (SQL only)
Automation levelManualSemi-automatedFully automated
Deployment difficultyHighHighMinimal

Step 1: Prepare Sample Data

First, let’s simulate an e-commerce sales dataset (you can replace this with real data):

import duckdb
from datetime import datetime, timedelta
import random

# Create DuckDB database
con = duckdb.connect("market_research.db")

# Generate simulated monthly category sales data (past 12 months)
data = []
categories = ["Digital Accessories", "Home Goods", "Beauty & Skincare", 
              "Sports & Outdoors", "Food & Beverages"]
months = [(datetime(2025, 8, 1) + timedelta(days=30*i)).strftime("%Y-%m") for i in range(12)]

random.seed(42)
for cat in categories:
    base_sales = random.randint(50000, 200000)
    for month in months:
        growth = random.uniform(-0.1, 0.25)  # -10% to +25% growth
        sales = int(base_sales * (1 + growth))
        orders = random.randint(sales // 200, sales // 50)
        avg_price = round(sales / max(orders, 1), 2)
        data.append((month, cat, sales, orders, avg_price))

# Write to table
con.execute("""
    CREATE TABLE monthly_sales AS
    SELECT * FROM data
    WITH (month VARCHAR, category VARCHAR, total_sales BIGINT, 
          order_count BIGINT, avg_price DOUBLE)
""")

print("✅ Sample data generated")
con.close()

This code generates simulated sales data for 5 categories × 12 months, including three core metrics: total sales, order count, and average unit price.

Step 2: Core Analysis Queries

This is the soul of the system — completing all key analyses with a few SQL queries.

SELECT 
    month,
    category,
    total_sales,
    ROUND(total_sales / LAG(total_sales) OVER (PARTITION BY category ORDER BY month) * 100 - 100, 1) AS mom_growth_pct
FROM monthly_sales
ORDER BY category, month;

Here we use the LAG() window function to calculate month-over-month growth rates — the core metric for identifying trends.

2. Monthly Category Rankings and Share Analysis

SELECT 
    category,
    total_sales,
    ROUND(100.0 * total_sales / SUM(total_sales) OVER (), 2) AS share_pct,
    RANK() OVER (ORDER BY total_sales DESC) AS rank_num
FROM monthly_sales
WHERE month = (SELECT MAX(month) FROM monthly_sales);

SUM() OVER () calculates total sales, while RANK() performs rankings. These window functions make complex aggregation analysis concise.

3. High-Growth Category Identification

WITH growth_calc AS (
    SELECT 
        category,
        month,
        total_sales,
        ROUND(total_sales / LAG(total_sales) OVER (PARTITION BY category ORDER BY month) * 100 - 100, 1) AS mom_growth
    FROM monthly_sales
),
consecutive_growth AS (
    SELECT 
        category,
        COUNT(*) AS consecutive_months_positive
    FROM growth_calc
    WHERE mom_growth > 0
    GROUP BY category
    HAVING COUNT(*) >= 3
)
SELECT 
    c.category,
    c.consecutive_months_positive,
    g.total_sales AS latest_sales,
    ROUND(AVG(g.mom_growth), 1) AS avg_growth_pct
FROM consecutive_growth c
JOIN growth_calc g ON c.category = g.category
WHERE g.month = (SELECT MAX(month) FROM monthly_sales)
ORDER BY g.mom_growth DESC;

Using CTEs (Common Table Expressions) to combine multi-layer logic, identifying “potential tracks” with positive growth for 3+ consecutive months.

4. Average Unit Price Trend

SELECT 
    category,
    ROUND(AVG(avg_price), 2) AS current_avg_price,
    ROUND(MIN(avg_price), 2) AS min_price_12m,
    ROUND(MAX(avg_price), 2) AS max_price_12m,
    ROUND(STDDEV(avg_price), 2) AS price_volatility
FROM monthly_sales
GROUP BY category
ORDER BY price_volatility DESC;

Categories with the highest price volatility often signal market opportunities or risk indicators.

Step 3: Auto-Generate Markdown Reports

This is where the real value lies — automatically assembling analysis results into a professional research report:

import duckdb
from datetime import datetime
from pathlib import Path

def generate_report(db_path="market_research.db", output_dir="./reports"):
    con = duckdb.connect(db_path)
    
    # Get latest month
    latest_month = con.execute(
        "SELECT MAX(month) FROM monthly_sales"
    ).fetchone()[0]
    prev_month = con.execute(
        "SELECT month FROM monthly_sales WHERE month < '{}' ORDER BY month DESC LIMIT 1".format(latest_month)
    ).fetchone()[0]
    
    # Build report content
    report = f"""# 📊 Market Trend Research Report

**Report Period**: {prev_month} ~ {latest_month}
**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M')}
**Data Source**: Public e-commerce sales data

---

## Key Findings

### 🔥 Three Key Trends This Period

"""
    
    # Trend 1: Top growing categories
    top_growth = con.execute("""
        SELECT category, total_sales, mom_growth_pct
        FROM (
            SELECT *,
                ROUND(total_sales / LAG(total_sales) OVER (PARTITION BY category ORDER BY month) * 100 - 100, 1) AS mom_growth_pct
            FROM monthly_sales
            WHERE month = '{latest}'
        )
        ORDER BY mom_growth_pct DESC
        LIMIT 3
    """.format(latest=latest_month)).fetchall()
    
    report += "#### 1. Top 3 High-Growth Categories\n\n"
    for row in top_growth:
        report += f"- **{row[0]}**: Monthly sales {row[1]:,}, MoM growth {row[2]}%\n"
    
    report += "\n"
    
    # Trend 2: Market share distribution
    shares = con.execute("""
        SELECT category, share_pct, rank_num
        FROM (
            SELECT 
                category,
                ROUND(100.0 * total_sales / SUM(total_sales) OVER (), 2) AS share_pct,
                RANK() OVER (ORDER BY total_sales DESC) AS rank_num
            FROM monthly_sales
            WHERE month = '{}'
        )
        ORDER BY rank_num
    """.format(latest_month)).fetchall()
    
    report += "#### 2. Market Share Distribution\n\n"
    for row in shares:
        bar = "█" * int(row[1] / 2)
        report += f"- {row[0]}: {row[1]}% {bar}\n"
    
    report += "\n"
    
    # Trend 3: Price volatility signal
    volatility = con.execute("""
        SELECT category, ROUND(AVG(avg_price), 2) AS current_price, price_volatility
        FROM (
            SELECT category, avg_price, 
                   ROUND(STDDEV(avg_price), 2) AS price_volatility
            FROM monthly_sales
            GROUP BY category
        )
        ORDER BY price_volatility DESC
        LIMIT 1
    """).fetchone()
    
    if volatility:
        report += f"#### 3. Price Volatility Signal\n\n"
        report += f"- **{volatility[0]}** has the highest price volatility (stddev {volatility[2]}), pricing strategy changes should be monitored\n\n"
    
    # Detailed data appendix
    report += "---\n\n## Appendix: Complete Data Summary\n\n"
    
    # Category summary
    summary = con.execute("""
        SELECT 
            category,
            SUM(total_sales) AS total_12m_sales,
            AVG(mom_growth_pct) AS avg_mom_growth,
            ROUND(AVG(avg_price), 2) AS avg_unit_price
        FROM (
            SELECT *,
                ROUND(total_sales / LAG(total_sales) OVER (PARTITION BY category ORDER BY month) * 100 - 100, 1) AS mom_growth_pct
            FROM monthly_sales
        )
        GROUP BY category
        ORDER BY total_12m_sales DESC
    """).fetchall()
    
    report += "| Category | 12-Month Total | Avg MoM Growth | Avg Unit Price |\n"
    report += "|----------|---------------|----------------|----------------|\n"
    for row in summary:
        report += f"| {row[0]} | {row[1]:,} | {row[2]:.1f}% | ¥{row[3]} |\n"
    
    # Save report
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    report_path = Path(output_dir) / f"report_{latest_month}.md"
    report_path.write_text(report, encoding="utf-8")
    print(f"✅ Report saved: {report_path}")
    
    con.close()

generate_report()

Advanced: Extending to Real Data Sources

The above example uses simulated data, but in practice, you can easily integrate various data sources:

Reading from CSV Files

# Read local CSV directly without loading into memory
con.read_csv_auto("sales_data.csv")

Fetching JSON Data from HTTP API

import requests

response = requests.get("https://api.example.com/sales")
data = response.json()

# Register JSON data as DuckDB table
con.register('api_data', data)

Querying Parquet Files

# Parquet is ideal for storing large-scale historical data
con.execute("SELECT * FROM read_parquet('sales_*.parquet')")

Monetization Ideas

The commercial value of this automated report generator is significant:

  1. Cross-border E-commerce Product Selection Reports: Sell each for 99-499 yuan to Amazon/eBay/Shopify sellers
  2. Industry Trend Weekly Subscriptions: Subscription model at 299-999 yuan/month for investment firms
  3. Enterprise Internal Data Products: Provide monthly business analysis reports for traditional companies
  4. SaaS Backend Engine: Use as the analysis engine for data products, charging per call

The key is to identify your target customer group, then use DuckDB to quickly build a minimum viable product (MVP). The entire system from development to deployment can be completed by an experienced developer in one day.

Summary

Building an automated market research report generator with DuckDB offers these core advantages:

  • SQL-driven: All analysis logic expressed in SQL — concise and maintainable
  • High performance: Vectorized execution engine responds in seconds for millions of records
  • Easy deployment: A single Python script runs everything, no complex infrastructure needed
  • Extensible: Supports CSV/JSON/Parquet/HTTP and multiple data sources

If you’re looking for a direction to monetize your technical skills, this solution is worth exploring deeply.

💡 Want to dive deeper into DuckDB for data products? duckdblab.org has a complete tutorial series covering everything from beginner to advanced实战.

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

Built with Hugo
Theme Stack designed by Jimmy

⚠️ This site is an independent community project, not affiliated with, endorsed by, or sponsored by the DuckDB Foundation or official DuckDB project.

"DuckDB" is a registered trademark of the DuckDB Foundation. This site uses the name solely for factual description purposes.

All content is for educational and community promotion purposes only and does not constitute any commercial service.