KajoData / python en
Python basics: from a variable to a checked pandas report
I follow one complete example from reading orders to checking inputs and explaining a grouped report without hiding missing amounts.

I find Python easier to learn through a task with a result I can check. A list of types and functions quickly becomes a vocabulary exercise. Here, I will build a small report: how many paid orders belong to each channel, and what is the sum of their known amounts? Along the way, we will use variables, lists, dictionaries, a function and basic pandas operations.
The data is synthetic and deliberately a little inconvenient. It includes a zero amount, missing amounts and a cancelled order. These cases show why code that runs still needs an interpretation check. You do not need to learn the entire library before starting. You need to understand each step and trace a reported number back to the records that produced it.
I checked these examples with Python 3.14.7 and pandas 3.0.6. Run the Python blocks in order in one script or a fresh session. Installation commands belong in a terminal, not inside a .py file. That small distinction can stop a first exercise before the data even arrives, so it is worth making the setup explicit.
Prepare a separate environment for the exercise
Start with an empty practice folder. I assume the python command launches an installed interpreter. Check its version before installing packages. If the command is unavailable, resolve the installation or interpreter selection first. Repeating package installation in several terminals tends to create more uncertainty about which environment you are actually using.
These commands are for PowerShell on Windows. Calling the environment’s interpreter directly avoids needing to activate it or change script execution settings. The Python virtual environment guide explains dependency isolation. On macOS and Linux, the environment’s interpreter normally lives under bin instead of Scripts, so the executable path differs.
python --version
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install pandas==3.0.6
.\.venv\Scripts\python.exe analysis.py
Create analysis.py in the practice folder and add the Python blocks below. Pinning pandas makes this exercise’s environment reproducible; it is not a recommendation to force that version on every project. Anaconda can also be useful, particularly in an established team environment. For this small task, a Python installation, a separate environment and one library are sufficient.
A variable gives a value a useful name
A variable lets you refer to an object by name. Python does not require you to declare a separate fixed-type container beforehand. Assignment binds the name to a value, and the object has a type. In practice, choose names that describe what the data represents. amount_cents is much more helpful than x when you return to a calculation later.
# example: values
order_id = "001"
amount_cents = 12000
is_paid = True
missing_amount = None
print(order_id, amount_cents / 100, is_paid, missing_amount)
The output is 001 120.0 True None. I keep the identifier as text to preserve its leading zeros. The amount is an integer number of cents, so summing these example amounts does not require representing decimal fractions. The Boolean describes a condition, while None indicates an absent value. These meanings remain distinct even if they all fit into spreadsheet cells.
Do not treat None, zero and an empty string as interchangeable. Zero can be a valid amount. A missing amount means we do not know it. An empty string may be an export convention. Decide what the values mean from the data and process, then express that decision in code. My article on variables, data types and operators develops these foundations further.
Lists, dictionaries and tuples organise information
A list holds an ordered collection. A dictionary associates keys with values and works well for a small record. A tuple can represent a fixed collection, such as expected column names. Python’s data structures tutorial explains their behaviour in detail. For now, the useful point is how they let us organise and name information.
# example: structures
orders = [
{"order_id": "001", "channel": "web", "amount_cents": 12000},
{"order_id": "002", "channel": "store", "amount_cents": 0},
]
columns = ("order_id", "channel", "amount_cents")
print(orders[0]["order_id"])
print(sum(order["amount_cents"] for order in orders))
This prints 001 and 12000. The first list element has index zero. Looking up a dictionary key retrieves a specific piece of information. The expression inside sum visits the records and selects each amount. A missing key produces an error worth understanding. Automatically replacing everything with zero could hide a problem in the input structure.
A tuple does not let you replace its elements, but it can contain objects that are themselves mutable. You do not need a complete theory of mutability for this exercise. Just avoid assuming that placing an object inside a tuple makes everything inside that object permanently unchangeable. Clear data structures and deliberate choices are enough to get started.
Use one complete input dataset
Our report contains six orders. Each row represents one order, and the identifier must be unique. The channels are web, store and partner. A paid status includes the order in the report; cancelled excludes it. These are exercise rules. In an actual project, agree on them with the person responsible for the process before calculating results.
# example: input
from io import StringIO
import pandas as pd
csv_text = """order_id,channel,status,amount_cents
001,web,paid,12000
002,store,paid,0
003,web,cancelled,9000
004,store,paid,
005,web,paid,8000
006,partner,paid,
"""
df = pd.read_csv(
StringIO(csv_text),
dtype={"order_id": "string", "channel": "string",
"status": "string", "amount_cents": "Int64"},
)
print(df.shape)
The output (6, 4) means six rows and four columns. StringIO lets us treat the text like a file, so there is no separate download. Later, you can supply a file path to read_csv. First run this exact dataset. That separates learning the transformations from diagnosing an unfamiliar export format at the same time.
The capitalised Int64 is a pandas integer dtype that supports missing values. Explicit dtypes also keep the identifier as text. The read_csv documentation covers separators, encoding and missing-value recognition. Default rules may interpret strings such as NA as missing. Check whether such a string is a legitimate code in your own source before accepting that behaviour.
Inspect the table before calculating
A DataFrame resembles a table with named columns and a row index. Creating it does not verify every business rule. It can successfully load repeated records, unexpected statuses or data from the wrong period. Before grouping, inspect the dimensions, types and some complete rows. Successful parsing and a suitable dataset are different questions that deserve separate checks.
# example: inspect
print(df.head(3).to_string(index=False))
print(df.isna().sum().to_dict())
print(df["order_id"].duplicated().sum())
Our dataset contains two missing amounts, no missing values in the other columns and no repeated identifiers. Looking at the first three rows helps check the format but does not establish the quality of the entire table. That is why I separately count missing values and duplicates across all rows. A sample and a full-table check provide different evidence.
If a check finds a repeated identifier, do not automatically delete it. Perhaps the source contains order lines and your one-row-per-order assumption is wrong. In this exercise, a duplicate stops the report. In another dataset, repeated order identifiers could be legitimate. The code should implement an agreed rule rather than guess the business meaning of records.
Put a repeatable operation into a function
A function accepts input, performs defined work and returns a result. Instead of copying filters into several places, I will put the report into one function. It first checks required columns and basic assumptions, then selects paid orders. This makes the boundary between input validation and calculation visible. A future reader can inspect both parts without reconstructing a chain of unrelated cells.
# example: report
def paid_by_channel(data):
required = {"order_id", "channel", "status", "amount_cents"}
if not required.issubset(data.columns):
raise ValueError("Missing required columns")
if data[["order_id", "channel", "status"]].isna().any().any():
raise ValueError("Missing identifier, channel or status")
if data["order_id"].duplicated().any():
raise ValueError("Duplicate order identifiers")
if not data["status"].isin(["paid", "cancelled"]).all():
raise ValueError("Unexpected status")
if (data["amount_cents"].dropna() < 0).any():
raise ValueError("Negative amount in this exercise")
paid = data.loc[data["status"].eq("paid")]
return paid.groupby("channel", sort=True).agg(
orders=("order_id", "size"),
known_amounts=("amount_cents", "count"),
known_amount_cents=("amount_cents",
lambda values: values.sum(min_count=1)),
)
report = paid_by_channel(df)
print(report.to_string())
Indentation is part of Python syntax. raise stops execution when a condition violates the exercise rules. return passes the resulting table back to the caller. I deliberately use descriptive output names. The sum of known amounts is not necessarily the complete value of paid orders when some amounts are absent.
This function assumes the dtypes established by the CSV-reading block. It is not a universal validator for arbitrary files. For example, it does not check every possible identifier format or detect whitespace-only identifiers. Negative amounts are rejected because the exercise does not model refunds. A project that includes refunds would need a different rule rather than a copy of this restriction.
Understand what filtering and grouping do
The expression data["status"].eq("paid") produces Boolean values. loc selects the matching rows. Then groupby divides them by channel, and agg specifies the measures for each group. We do not need a separate loop for web and store. Another valid category can appear as an additional output group without rewriting the function.
The groupby documentation describes sorting and missing-key options. Here, sorting is explicit, while an earlier check rejects missing channels. We therefore avoid silently losing an order with no category. The original version of this article grouped by city even though the displayed input did not create that column. This version uses one complete dataset throughout.
size counts rows; count on a selected column counts nonmissing values. That distinction matters for store, where two orders have only one known amount. Summing with min_count=1 keeps the result missing when a group has no known amount at all. Without an explicit choice, an unknown result can easily be mistaken for a genuine zero.

Read the result together with its limitation
| Channel | Orders | Known amounts | Sum (cents) |
|---|---|---|---|
| partner | 1 | 0 | missing |
| store | 2 | 1 | 0 |
| web | 2 | 2 | 20000 |
The web channel has two paid orders with complete amounts: 12000 and 8000 cents. The cancelled order with an amount of 9000 is excluded. For store, one known amount is zero and the other is unknown. For partner, no amount is known. This table does not establish that the partner channel sold nothing.
Across the report, there are five paid orders, three known amounts and a known-amount sum of 20000 cents. That equals 200 currency units in this example. It is not a verified complete value for all five orders. When you communicate the result, the two missing amounts should travel with the total rather than remain hidden in your notebook.
Notice how much interpretation happens after the code finishes. You could build a chart from the table, but its title and labels would need to preserve the distinction between a partial amount and a complete total. A more sophisticated visual would not fix an inaccurate description. First make the small table understandable; then decide whether a chart would help the recipient.
Check known expectations with assertions
An assertion expresses an expectation and raises an error when it is false. I do not use assertions instead of the function’s explicit validation, because Python can disable assertions in optimised execution. Here, they provide concise checks for a known exercise. A production reporting process needs an appropriate strategy for reporting and handling validation failures.
# example: checks
assert report["orders"].sum() == 5
assert report["known_amounts"].sum() == 3
assert report.loc["web", "known_amount_cents"] == 20000
assert report.loc["store", "known_amount_cents"] == 0
assert pd.isna(report.loc["partner", "known_amount_cents"])
assert paid_by_channel(df.iloc[:0]).empty
The empty-table check challenges the assumption that a report always contains a result. Also try setting every status to cancelled: the output should be empty. Adding a duplicate identifier should stop the function. These cases are more informative than repeatedly processing the same six rows. They exercise boundaries where reporting mistakes often become visible.
Predict the expected behaviour before changing the input. If you only inspect the output afterwards, it is tempting to accept whatever the program produces. Writing an expectation first gives you an independent point of comparison. The point is not to make every test pass by changing the test. It is to understand whether the rule and implementation agree.
Diagnose an error at the right stage
Read the exception name and the last lines of the traceback. KeyError often directs attention to a missing key or column. A ValueError raised by this function identifies a violated assumption. ModuleNotFoundError when importing pandas may mean that a different interpreter is running from the one where you installed the package. Start with the actual symptom instead of rewriting the whole program.
Reduce the input to a few rows that still reproduce the problem. Print column names and dtypes. Check the separator. If all fields arrived in one column, changing the grouping operation will not repair the import. Working through these stages helps distinguish an input problem, a transformation problem and a misunderstanding of the final result.
In a notebook, restart and run all cells from the beginning. An old variable can survive edits to an earlier cell and make the output depend on the order of your clicks. A script that works in a fresh session gives you a more reliable reference. When asking for help, provide a small synthetic input, the relevant code and the expected output, without customer data.
Move to your own input file
Before switching to your own file, describe its encoding, separator, amount units and date coverage. Identical column names do not guarantee identical meanings. Work on a practice copy and preserve the original. If you correct values, record the rule rather than silently editing a few cells. A recurring problem needs an understood transformation; an unexplained value needs investigation.
Give exported results a separate name and record their scope. Keep all three report measures rather than exporting only the final amount column. Without the order count and known-amount count, the recipient cannot see differences in completeness between channels. Include the status filter in a short accompanying note so another person can understand what the report covers.
Read unfamiliar code through its inputs and outputs
Take one operation at a time. There are six rows before filtering, five afterwards and three groups after aggregation. Every change in row count should have an explanation. Inspect these intermediate points as you extend the example. That is easier than searching for an error in the import, filter and aggregation simultaneously.
You do not need to memorise every library argument. Learn to find the relevant documentation and ask a specific question about the return type, missing-value behaviour or defaults. Then explain the function without Python terminology: check assumptions, select paid orders, group by channel and show amount completeness. If that description matches the intended analysis, the syntax has a clear job to do.
Change the question while keeping the checks
Add order 007 in channel web, with status paid and amount 5000. First predict the result: six paid orders, four known amounts, a known-amount total of 25000 and three orders in web. Then run the code. If the result differs, inspect the records and find the cause instead of replacing your expectation with whatever appeared on screen.
Next, plan a monthly version by adding a date column. Decide which event determines the month and what a missing date means. You do not need to build a more complicated function immediately. Write the rule, create boundary records and calculate a small expected result manually. That lets you learn Python alongside the design of an analysis.
You now have a complete sequence: input, checks, selection, grouping and result verification. New language features will have somewhere useful to fit. I would return to this code after a few days and explain it without consulting the article. The parts you cannot explain will identify a sensible topic for your next practice session.
Key takeaways
Do I need Anaconda for this exercise?
No. Python, a separate venv environment and pandas are sufficient. In an existing team, use the agreed environment.
How do size and count differ when grouping?
size counts rows, while count on a selected column counts nonmissing values. An order count can therefore exceed the count of known amounts.
Can I replace a missing amount with zero?
Only when the data definition supports it. Here a missing amount is unknown, so the report preserves it and shows completeness.
