Free AI CSV Analyzer: Upload a CSV, Get an Answer About Your Data
Updated October 2026
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:
- A CSV file with your data
- Basic understanding of what's in your file (sales records, customer data, inventory, etc.)
- Python 3 with pandas and NumPy, for the code in this tutorial (the free tool in Step 1 needs no code)
- A web browser (Chrome, Firefox, Safari, or Edge)
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:
- Header row: the first row holds the column names, one per column
- One row per record: for a Shopify order export that is one row per line item, so an order with three products takes three rows
- Plain numbers: remove currency symbols and thousands separators so a column such as
Totalreads as a number - Only the columns you need: names, emails and addresses add size and are rarely needed for a first look
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:
- Row and column counts
- The type of each column (numbers, dates, text, categories)
- What is missing, column by column
- Duplicate rows
- How the numbers are spread: mean, median, standard deviation, and whether a few large values pull the mean up
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.
- Open the free data profile tool
- Upload your CSV. The tool page says the free analysis covers files up to 10,000 rows
- 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.
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.