ds@mcpanalytics.aiEmail your data and your question and get an answer about your own data, with the method shown.Email your data and question. Get an answer.

Free AI CSV Analyzer: Upload a CSV, Get an Answer About Your Data

Updated October 2026

By MCP Analytics Team

Business professionals often hit a wall when they try to look at their own data: you export a sales report, a customer list or an inventory file, and you are not sure what to check first. This tutorial shows what a first look at a CSV should cover and gives you short pandas code you can run on your own export. Before we build any complex models, let's just look at what your data is telling you.

Prerequisites

Before we start, here's what you'll need:

If your data lives in Excel, use "Save As" and choose "CSV (Comma delimited)". Database dumps and Shopify order exports are usually CSV already.

Check Your File Before You Upload

Large exports are slow to open and slow to upload, so decide what you need before you export. Four checks save most of the trouble:

What a First Look Should Show

The moment you have a file, ask what is in it before you ask what it means. A first profile covers:

John Tukey's line about pictures applies here: "The greatest value of a picture is when it forces us to notice what we never expected to see." A profile is the cheap way to find what you did not expect before you build anything on top of the file.

Step 1: Get a First Profile With the Free Tool

The free data profile tool does this first pass for a CSV you upload.

  1. Open the free data profile tool
  2. Upload your CSV. The tool page says the free analysis covers files up to 10,000 rows
  3. Enter your email address. The tool emails you a link to the report

The tool page describes the report as covering every column in your file: its type, what is missing, and how its numbers are spread, plus the fixes worth making before you analyse the file. If your file has more than 10,000 rows, profile a sample with the free tool and use the code below on the whole file.

Step 2: Profile the File Yourself

The remaining steps use a CSV you can reproduce. The first block builds a synthetic file of 6,000 order lines with Shopify-style column names. The figures in the outputs below come from this synthetic data set generated for the tutorial, not from a real store, so your numbers will differ. The columns are Name (order number), Email, Paid at, Lineitem quantity, Lineitem price, Lineitem compare-at price, Lineitem sku and Total. A blank compare-at price means the item was not marked down, and about 2% of emails are blank on purpose.

import numpy as np
import pandas as pd

rng = np.random.default_rng(42)
n_orders = 6000
customers = rng.integers(1, 3501, n_orders)          # customer ids 1..3500
dates = pd.to_datetime("2023-01-01") + pd.to_timedelta(rng.integers(0, 365, n_orders), unit="D")
price = rng.choice([19.0, 29.0, 49.0, 79.0, 129.0], n_orders, p=[0.3, 0.3, 0.2, 0.15, 0.05])
compare_at = np.where(rng.random(n_orders) < 0.35, np.round(price * rng.uniform(1.1, 1.6, n_orders), 2), np.nan)
qty = rng.integers(1, 4, n_orders)

df = pd.DataFrame({
    "Name": [f"#{1000 + i}" for i in range(n_orders)],
    "Email": [f"customer{c}@example.com" for c in customers],
    "Paid at": dates,
    "Lineitem quantity": qty,
    "Lineitem price": price,
    "Lineitem compare-at price": compare_at,
    "Lineitem sku": rng.choice(["SKU-1001", "SKU-1002", "SKU-1003", "SKU-1004"], n_orders),
})
df["Total"] = (df["Lineitem quantity"] * df["Lineitem price"]).round(2)
df.loc[rng.random(n_orders) < 0.02, "Email"] = np.nan   # a few missing emails
df.to_csv("orders_synthetic.csv", index=False)
print("wrote", len(df), "rows")

Expected Output

wrote 6000 rows

Now profile it. Always state the denominator when you quote a percentage: missing cells divided by rows times columns.

df = pd.read_csv("orders_synthetic.csv", parse_dates=["Paid at"])

cells = df.shape[0] * df.shape[1]
missing = int(df.isna().sum().sum())
print("Rows:", df.shape[0], " Columns:", df.shape[1], " Cells:", cells)
print("Missing cells:", missing, f"({missing / cells:.2%} of {cells} cells)")
print("Duplicate rows:", int(df.duplicated().sum()))
print()
print(df.isna().sum()[lambda s: s > 0].to_string())
print()
print(df["Total"].describe(percentiles=[0.25, 0.5, 0.75]).round(2).to_string())
print("Skew:", round(df["Total"].skew(), 2))

Expected Output

Rows: 6000  Columns: 8  Cells: 48000
Missing cells: 4021 (8.38% of 48000 cells)
Duplicate rows: 0

Email                         121
Lineitem compare-at price    3900

count    6000.00
mean       85.06
std        71.12
min        19.00
25%        38.00
50%        58.00
75%        98.00
max       387.00
Skew: 2.04

Read the counts before the percentage. The file has 6,000 rows and 8 columns, so 48,000 cells, and 4,021 of them are empty, which is 8.38%. Almost all of that is the compare-at price (3,900 blanks), which is blank by design when an item is not discounted, so it is not a data problem. The 121 blank emails (about 2% of rows) are the gap worth knowing about, because you cannot attribute those orders to a customer.

The Total column has a mean of 85.06 and a median of 58.00. When the mean sits well above the median and the skew is positive (2.04 here), a few large orders pull the average up. Use the median to describe a typical order and the mean when you need the sum.

Step 3: Ask Follow-Up Questions of the Data

Once the profile looks sane, ask specific questions. Three examples follow, each as code you can edit for your own columns.

Repeat Versus One-Time Customers

A customer needs at least one order, so the number of customers can never exceed the number of orders. The assertion in the code checks that your counts agree before you quote them.

orders_per_customer = df.groupby("Email")["Name"].nunique()
n_customers = len(orders_per_customer)
repeat = int((orders_per_customer > 1).sum())
print("Orders with an email:", int(orders_per_customer.sum()))
print("Customers:", n_customers)
print("One-time:", n_customers - repeat, " Repeat:", repeat, f"({repeat / n_customers:.1%})")
assert orders_per_customer.sum() >= n_customers   # customers can never outnumber orders

by_group = df.dropna(subset=["Email"]).assign(
    group=lambda d: np.where(d["Email"].map(orders_per_customer) > 1, "repeat", "one-time"))
print()
print(by_group.groupby("group")["Total"].agg(["count", "mean"]).round(2).to_string())

Expected Output

Orders with an email: 5879
Customers: 2822
One-time: 1054  Repeat: 1768 (62.7%)

          count   mean
group                 
one-time   1054  86.64
repeat     4825  84.59

In this synthetic file 1,768 of 2,822 customers (62.7%) ordered more than once, and the average order is about the same for both groups (86.64 and 84.59). The synthetic generator draws customer ids at random, so treat these figures as a demonstration of the calculation, not as a benchmark.

Discounts From the Compare-At Price

The compare-at price is the price an item is marked down from, so the discount on a line is one minus price divided by compare-at price. This uses only columns in the export. It cannot tell you about conversion or profit margin: conversion needs visits or sessions, and margin needs product costs, and neither is in an order export.

has_cmp = df["Lineitem compare-at price"].notna()
disc = 1 - df.loc[has_cmp, "Lineitem price"] / df.loc[has_cmp, "Lineitem compare-at price"]
print("Lines with a compare-at price:", int(has_cmp.sum()), f"({has_cmp.mean():.1%} of lines)")
print("Discount vs compare-at: mean", f"{disc.mean():.1%}", " min", f"{disc.min():.1%}", " max", f"{disc.max():.1%}")
print()
print(df.groupby(has_cmp.rename("has_compare_at"))["Total"].agg(["count", "mean"]).round(2).to_string())

Expected Output

Lines with a compare-at price: 2100 (35.0% of lines)
Discount vs compare-at: mean 25.4%  min 9.1%  max 37.5%

                count   mean
has_compare_at              
False            3900  84.94
True             2100  85.29

In this file 2,100 lines (35.0%) carry a compare-at price, with discounts between 9.1% and 37.5% and a mean of 25.4%. Average order totals are close for lines with and without a compare-at price (85.29 against 84.94). With real data, a difference like that would still be an association: shoppers who see a markdown differ from those who do not in ways the export does not record.

Revenue by Month

To check for seasonality, add up the total by month and compare each month with the average month. One year of data can show a pattern but cannot separate a seasonal pattern from a one-off year, so look at two or more years before planning around it.

monthly = df.set_index("Paid at")["Total"].resample("MS").sum()
share = (monthly / monthly.sum() * 100).round(1)
print(pd.DataFrame({"revenue": monthly.round(0).astype(int), "share_%": share}).to_string())
print("Months above 1.2x the average month:", [d.strftime("%b") for d in monthly.index[monthly > 1.2 * monthly.mean()]])

Expected Output

            revenue  share_%
Paid at                     
2023-01-01    43014      8.4
2023-02-01    41822      8.2
2023-03-01    41302      8.1
2023-04-01    45226      8.9
2023-05-01    40961      8.0
2023-06-01    44047      8.6
2023-07-01    44584      8.7
2023-08-01    48967      9.6
2023-09-01    41322      8.1
2023-10-01    40476      7.9
2023-11-01    37682      7.4
2023-12-01    40965      8.0
Months above 1.2x the average month: []

Monthly shares run from 7.4% to 9.6% of annual revenue and no month exceeds 1.2 times the average month, so this synthetic year has no seasonal peak, which is what you expect from dates drawn uniformly. A real store with a strong Q4 would show up as a few months with much larger shares. A forecast needs more than a monthly total: it needs a fitted model checked against months it did not see, and R² on the months it was fitted to is not a measure of forecast reliability.

Step 4: Turn a Number Into a Decision

For inventory, a reorder point tells you when to place the next order. The standard formula is average daily sales times lead time, plus safety stock. This example assumes a 7 day lead time and safety stock of 1.5 times average weekly demand. The stock levels are invented, because the order export has no stock column. Daily sales come from the last 90 days of the synthetic orders.

last90 = df[df["Paid at"] >= df["Paid at"].max() - pd.DateOffset(days=90)]
daily = last90.groupby("Lineitem sku")["Lineitem quantity"].sum() / 90
stock = pd.Series({"SKU-1001": 47, "SKU-1002": 234, "SKU-1003": 12, "SKU-1004": 89})  # invented stock levels
lead_time_days = 7
safety = 1.5 * daily * 7                      # 1.5 x average weekly demand
table = pd.DataFrame({"stock": stock, "daily_sales": daily.round(2)})
table["reorder_point"] = (daily * lead_time_days + safety).round(1)
table["status"] = np.where(table["stock"] <= table["reorder_point"], "ORDER NOW", "ok")
table["days_of_stock"] = (table["stock"] / daily).round(1)
print(table.to_string())

Expected Output

          stock  daily_sales  reorder_point     status  days_of_stock
SKU-1001     47         7.58          132.6  ORDER NOW            6.2
SKU-1002    234         8.23          144.1         ok           28.4
SKU-1003     12         8.69          152.1  ORDER NOW            1.4
SKU-1004     89         7.41          129.7  ORDER NOW           12.0

With about 7.5 to 8.7 units sold per day, the reorder point is roughly 130 to 150 units for each SKU: 7 days of sales plus 10.5 days of safety stock. Three of the four SKUs are at or below it. SKU-1003 has 12 units left, about 1.4 days of sales. Change the lead time and the safety factor to match your supplier before you act on a table like this.

Troubleshooting Common Issues

Most problems come from the file, not from the analysis. Common ones:

Issue: A Number Column Reads as Text

Remove currency symbols and thousands separators, or convert in pandas:

df["Total"] = pd.to_numeric(df["Total"].astype(str).str.replace(r"[^0-9.\-]", "", regex=True))

Issue: A Date Column Reads as Text

Tell pandas which columns are dates when you read the file, as in the code above, and keep the format consistent (YYYY-MM-DD is the least ambiguous).

Issue: The File Opens as One Column

The file may use semicolons or tabs. Check the first line in a text editor and pass sep=";" to pd.read_csv if needed. A file saved as .xlsx and renamed to .csv will not parse.

Run It on Your Own Shopify Export

Export your orders from Shopify as a CSV and upload it at MCP Analytics. The analysis runs a data profile on your file and shows the result with the method used. You choose the columns; nothing connects to your Shopify account.

Try it on your data

Your turn

Bring your own data and the question you actually need answered.

MCP AnalyticsData science by email Send us your data and your question, and we will reply. ds@mcpanalytics.ai

Frequently Asked Questions

Can I use sensitive data?

Remove or pseudonymize personal data before you upload anything. For a first look you rarely need names or emails: hash the email column, or replace it with a customer id, and the repeat-customer calculation above still works.

Do I need to clean my data before analyzing it?

Do the checks in this tutorial first. Missing values, text in number columns and duplicate rows change the results, and the profile in Step 2 shows you where they are. Garbage in, garbage out still applies.

Conclusion

The goal of analytics isn't to create pretty charts or impressive statistics. It's to make better decisions. Start simple: profile the file, state the denominator behind every percentage, ask one specific question at a time, and say what each number can and cannot show.