Analyzing an Excel or CSV File with AI locale
Analyzing an Excel or CSV file with local AI doesn't require handing the addition over to the model. This guide splits the work: pandas reads the file and does the calculations, a second script recounts by a different route, and then a model served by Ollama comments on results that have already been checked. The complete code is provided for a CSV, along with the changes needed for an Excel workbook. One section states exactly what was verified in the field and what wasn't.
#Analyzing an Excel or CSV File with Local AI: Who Calculates, Who Interprets
Pasting a table into a chat is tempting: the answer comes back in sentences, with totals that look right. But an LLM generates plausible text and doesn't run a calculation when it writes a total. Nothing in its answer distinguishes a correct figure from a wrong one. And the longer the file, the more likely it is to overflow the model's context, sometimes without a clear message.
The split proposed here is simple: anything that can be recalculated identically goes to a deterministic tool. The same file and the same script give the same result, and that result can be checked. The model keeps what it's good at: putting results into words, flagging what deserves a look, and suggesting questions.
- Read and validate: pandas
- Columns present, empty cells, numeric amounts, dates in the expected format. The script stops instead of guessing.
- Calculate: pandas
- Totals and shares by category, written to a faits.json file.
- Check: Python alone
- A second script rereads the source file without pandas and recounts. Any discrepancy blocks the next steps.
- Interpret: the local model
- It receives only faits.json, not the file's rows, and writes a commentary without writing a single number itself.
- Decide: you
- A total proves a sum, not a cause. The model's commentary remains a lead to investigate.
#How this differs from the text-to-SQL tutorial
Benefit from our experience and a guided path to build your local assistant and use your AI every day.
- Lifetime online access
- PDF + files
- Refunded within 30 days
QuelLLM already offers a local text-to-SQL tutorial. Both guides prevent the LLM from inventing results, but they don't give it the same role. The choice depends mainly on your need: asking varied questions or rerunning a known calculation.
- Text-to-SQL: the model's role
- It writes the query from a question in French. The database then runs this generated code, hence a read-only role and SQL validation.
- This guide: the model's role
- It writes no code and runs nothing. You write the calculation once, then the model comments on its result.
- Text-to-SQL: when to choose it
- Data in PostgreSQL or MySQL, and questions that change often.
- This guide: when to choose it
- An export or a workbook with a recurring calculation: monthly report, breakdown by category, expense tracking.
- Limits of this guide
- A new question means modifying the script. That's the price of a reviewed, reproducible calculation.
#Prerequisites and sample file
- Ollama
- The service must respond on http://localhost:11434. The installation guide is listed at the end of the page.
- Python
- A virtual environment with Python 3.11 or later for pandas 3.
- A model
- The example uses qwen3.5:9b, about 6.6 GB in Q4, a size that fits on a RTX 3060 12GB. It reuses the model from the guide on structured outputs; the quality of its commentary on tabular data wasn't evaluated here.
- A dummy file
- Start with the small file below. You can check its totals in your head, which lets you see whether the setup works before giving it real data.
This format is common in a French export: semicolons between columns, commas for decimals, dates in day/month/year order. Check yours in a text editor, not in Excel, which reformats the display.
#Step 1: calculate with pandas
The script explicitly describes the file format and would rather stop than produce a questionable total. The pandas documentation, accessed on 2026-11-03 (version 3.1.0), describes the parameters used: sep for the delimiter, decimal for European data, and on_bad_lines, whose default value, "error", raises an error on a malformed line.
Two choices deserve an explanation. First, keep_default_na=False: pandas's default list treats NA, N/A, and null, among others, as missing values. A category named "NA" read as missing could vanish from a grouping without warning. Second, duplicates are counted but not removed: two identical purchases on the same day may be legitimate, and that decision is yours.
For the sample file, you can calculate the result by hand: A = 100 + 25 = 125, B = 50, for a sum of 175. Your output should match the following values.
#Step 2: Recount another way
A deterministic calculation can be wrong in a perfectly reproducible way: wrong separator, misread column, forgotten filter. So this second script doesn't trust pandas: it rereads the source file with the standard csv and decimal modules, recounts, then compares. This is a check of reading and addition. It doesn't tell you whether the calculation you chose answers your question.
Chain the steps with &&: if one fails, the next doesn't start. The model will therefore never see results rejected by the check.
#Step 3: Have the model interpret the results
The model receives the contents of faits.json and responds according to a JSON schema, using Ollama's format parameter, explained in the guide on structured outputs. This schema sets the shape of the response, not its accuracy. The useful safeguard lies elsewhere: the model sees the values so it can compare them, but must cite them by their key. The remplir function rejects any figure written directly as well as any invented key.
This guard doesn't cover everything. It doesn't detect a number written out in words (“three times more”), or a false conclusion built from correct values, such as a cause presented as established. It blocks copied or invented figures, but human review is still necessary. If a response is rejected, don't fix it by hand: rerun or reword the prompt, then keep a record of the rejection.
Sending only aggregates also reduces what the model can read: no customer names and no free text from the rows. Category labels are still sent, however, and can contain hostile text if the file comes from outside. Here, the model has no tools and can only produce text, which limits the possible damage without eliminating the risk.
#Step 4: Analyzing an Excel file, what changes
For an .xlsx workbook, only the file-reading step changes in calculs.py: read_excel replaces read_csv, and the sep, decimal, encoding, and on_bad_lines parameters go away. This block has not been run on an Excel file, so try it first on a small workbook whose totals you already know.
- Engine
- In the 3.1.0 documentation, openpyxl is still the default for .xlsx, but this default is announced as switching to calamine in a future version. Naming it explicitly keeps you from changing engines unknowingly during an update.
- Feuille
- sheet_name defaults to 0, which is the first sheet. With None or a list, read_excel returns a dictionary of DataFrames rather than a single one: the script above would then need to be adapted.
- Header
- The script assumes the column names are in the first row. If a title precedes the table, use the header parameter or, better, clean up a copy of the workbook.
- Dates
- If the column contains real Excel dates, the script's day/month/year format doesn't necessarily apply to them. Check date_min and date_max in faits.json.
- Total rows
- A “Total” row at the bottom of the sheet would be counted as a category and would double the sum. It then appears as a key in faits.json, so review that list.
The check in step 2 reads a CSV. To keep it, export the sheet from Excel (Save As, CSV UTF-8 format), then run python controle.py export.csv. The export goes through Excel rather than pandas's reading engine: it's a different read path, and discrepancies are exactly what you're looking for. Check the delimiter: the script expects a semicolon. An amount exported with its display format, such as "1 250,00 €", will make the check fail rather than slip through unnoticed: remove that format in the copy you export. A display format that rounds values can also create a discrepancy, to be handled the same way.
#What was verified, and what wasn't
On October 30, 2026, a dummy CSV was read and aggregated with pandas 3.0.2; the check returned A=125 and B=50. This is the only field evidence in this guide. The sample file above reuses these totals to stay readable, but that doesn't prove the published scripts work.
- Verified
- Reading and aggregating a dummy CSV with pandas 3.0.2, totals A=125 and B=50 (October 30, 2026).
- Reviewed, not run
- Parameters for read_csv and read_excel, in the pandas 3.1.0 documentation accessed November 3, 2026. It's newer than the version used in the test: check your version with pip show pandas.
- Unverified
- Full execution of calculs.py, controle.py, and interpreter.py in their published form, and the values output to faits.json: the ones shown are calculated by hand.
- Unverified
- Any LLM call: quality of the commentary, adherence to the prompt, frequency of rejections. No sample response is therefore shown.
- Unverified
- Any reading of an Excel file, including formulas and CSV export.
No timings, error rates, or model comparisons appear here, for lack of measurements. Treat these scripts as a starting point to try on the dummy file before using your own data. A useful test also involves triggering failures: change an amount in faits.json and the check must refuse the result; give the fill step a text containing a number and the response must be rejected.
#Troubleshooting
- "The montant column contains something other than numbers"
- A currency symbol, some text, or a thousands separator remains. If the file uses a thousands separator, add the thousands parameter to read_csv with the correct character. In French files, it's often a non-breaking space, invisible to the eye. The check will then need to remove that same character before Decimal.
- Only one column read, or columns missing
- The delimiter is wrong. Open the file in a text editor, then adjust sep in calculs.py and delimiter in controle.py.
- UnicodeDecodeError
- The file isn't in UTF-8. An old Windows export is often in cp1252: change encoding in both scripts, then check the accents in the category names.
- Date format error
- A date doesn't follow day/month/year. Correct the data or adjust format. Avoid a permissive conversion, which can hide a swapped day and month.
- Inconsistent shares
- With refunds or negative amounts, a percentage share can exceed 100 or lose its meaning. The script doesn't flag this: in that case, remove those shares from faits.json.
- Response rejected on every attempt
- The model copies the values despite the instruction. Rephrase it or try another model, without removing the guard.
- “Connection refused” on the Ollama side
- The service isn't running. Check with ollama list and the address http://localhost:11434.
#Go further
These guides extend this setup: text-to-SQL if your data is in a database, structured outputs for the format parameter, and the risks of instructions hidden in files from outside.
Feedback, an error, or a clarification? Let us know—it improves the guide for everyone.