Mathew K Analytics

Lesson 7 · Pandas Full Course

Read and Write CSV, Excel, JSON and SQL in Pandas (No More File Errors) | Pandas Course #7

In this lesson you'll be able to: Read CSV files with the right columns and real dates Save results to CSV and Excel, and read them back safely Turn a…

What you'll learn

Datasets used in this lesson

Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.

📓 Full notebook

Download .ipynb

Pandas 07: Importing and Exporting Data (CSV, Excel, JSON, SQL)#

In this lesson you'll be able to:

  • Read CSV files with the right columns and real dates
  • Save results to CSV and Excel, and read them back safely
  • Turn a nested API-style JSON response into a flat table
  • Query a SQL database straight into a DataFrame
  • Fix file encoding errors when text looks broken Dataset: Superstore orders (data/pandas/superstore.csv)

Warm-up: load and explore#

Lesson 06 created and explored DataFrames. Same moves, real file.

import pandas as pd
sales = pd.read_csv("data/pandas/superstore.csv")
print(sales.shape)
sales[["Order ID", "Order Date", "Region", "Sales"]].head(3)
(8399, 21)
Order ID Order Date Region Sales
0 3 10/13/2010 Nunavut 261.54
1 293 10/1/2012 Nunavut 10123.02
2 293 10/1/2012 Nunavut 244.57
sales.dtypes.value_counts()
str        12
float64     6
int64       3
Name: count, dtype: int64

Read CSV files the smart way#

Problem: load only what you need, with dates as real dates.

print(sales["Order Date"].dtype)
print(sales["Order Date"].head(3).tolist())
str
['10/13/2010', '10/1/2012', '10/1/2012']
path = "data/pandas/superstore.csv"
cols = ["Order ID", "Order Date", "Region", "Customer Segment",
        "Sales", "Profit"]
orders = pd.read_csv(path, usecols=cols,
                     parse_dates=["Order Date"],
                     date_format="%m/%d/%Y")
orders.dtypes
Order ID                     int64
Order Date          datetime64[us]
Sales                      float64
Profit                     float64
Region                         str
Customer Segment               str
dtype: object
full_mb = sales.memory_usage(deep=True).sum() / 1e6
slim_mb = orders.memory_usage(deep=True).sum() / 1e6
print(f"all columns: {full_mb:.1f} MB, six columns: {slim_mb:.1f} MB")
all columns: 2.7 MB, six columns: 0.5 MB
peek = pd.read_csv(path, nrows=5)
print(peek.shape)
(5, 21)

Quick check: what date range do these orders cover?

print(orders["Order Date"].min())
print(orders["Order Date"].max())
2009-01-01 00:00:00
2012-12-30 00:00:00

Common mistake: a file path that doesn't match the folder

wrong = "data/superstore.csv"
pd.read_csv(wrong)
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[9], line 2
      1 wrong = "data/superstore.csv"
----> 2 pd.read_csv(wrong)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:872, in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, skip_blank_lines, parse_dates, date_format, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, on_bad_lines, low_memory, memory_map, float_precision, storage_options, dtype_backend)
    860 kwds_defaults = _refine_defaults_read(
    861     dialect,
    862     delimiter,
   (...)
    868     dtype_backend=dtype_backend,
    869 )
    870 kwds.update(kwds_defaults)
--> 872 return _read(filepath_or_buffer, kwds)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:300, in _read(filepath_or_buffer, kwds)
    297 _validate_names(kwds.get("names", None))
    299 # Create the parser.
--> 300 parser = TextFileReader(filepath_or_buffer, **kwds)
    302 if chunksize or iterator:
    303     return parser

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:1643, in TextFileReader.__init__(self, f, engine, **kwds)
   1640     self.options["has_index_names"] = kwds["has_index_names"]
   1642 self.handles: IOHandles | None = None
-> 1643 self._engine = self._make_engine(f, self.engine)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:1907, in TextFileReader._make_engine(self, f, engine)
   1905     if "b" not in mode:
   1906         mode += "b"
-> 1907 self.handles = get_handle(
   1908     f,
   1909     mode,
   1910     encoding=self.options.get("encoding", None),
   1911     compression=self.options.get("compression", None),
   1912     memory_map=self.options.get("memory_map", False),
   1913     is_text=is_text,
   1914     errors=self.options.get("encoding_errors", "strict"),
   1915     storage_options=self.options.get("storage_options", None),
   1916 )
   1917 assert self.handles is not None
   1918 f = self.handles.handle

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\common.py:930, in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
    925 elif isinstance(handle, str):
    926     # Check whether the filename is to be opened in binary mode.
    927     # Binary mode does not support 'encoding' and 'newline'.
    928     if ioargs.encoding and "b" not in ioargs.mode:
    929         # Encoding
--> 930         handle = open(
    931             handle,
    932             ioargs.mode,
    933             encoding=ioargs.encoding,
    934             errors=errors,
    935             newline="",
    936         )
    937     else:
    938         # Binary mode
    939         handle = open(handle, ioargs.mode)

FileNotFoundError: [Errno 2] No such file or directory: 'data/superstore.csv'
import os
print(os.path.exists(wrong))
print(os.path.exists(path))
print(sorted(os.listdir("data/pandas"))[:4])
False
True
['aapl.csv', 'diamonds.csv', 'flights.csv', 'gapminder.csv']

Save results to CSV and read them back#

Problem: share a result as a file that opens cleanly anywhere.

os.makedirs("output/pandas", exist_ok=True)
top = orders.nlargest(5, "Sales")
top.to_csv("output/pandas/l07_top_orders.csv")
top
Order ID Order Date Sales Profit Region Customer Segment
4674 29766 2009-03-21 89061.05 27220.69 Atlantic Consumer
3512 3073 2009-01-07 45923.76 102.61 Quebec Home Office
5861 30343 2012-05-21 41343.21 3852.19 Prarie Corporate
3572 14435 2012-12-01 33367.85 3992.52 Quebec Small Business
6995 32932 2010-01-09 29884.60 12748.86 West Corporate
back = pd.read_csv("output/pandas/l07_top_orders.csv")
print(back.columns.tolist())
['Unnamed: 0', 'Order ID', 'Order Date', 'Sales', 'Profit', 'Region', 'Customer Segment']

Quick check: where did the extra Unnamed: 0 column come from?

top.to_csv("output/pandas/l07_top_orders.csv", index=False)
back = pd.read_csv("output/pandas/l07_top_orders.csv",
                   parse_dates=["Order Date"])
print(back.columns.tolist())
print(back.equals(top.reset_index(drop=True)))
['Order ID', 'Order Date', 'Sales', 'Profit', 'Region', 'Customer Segment']
True

Your turn#

Save the 5 orders with the biggest losses to l07_losses.csv, without the index. Read it back and print its shape.

losses = orders.nsmallest(5, "Profit")
losses.to_csv("output/pandas/l07_losses.csv", index=False)
check = pd.read_csv("output/pandas/l07_losses.csv")
print(check.shape)
check[["Order ID", "Region", "Profit"]]
(5, 6)
Order ID Region Profit
0 37095 West -14140.70
1 43044 West -12558.00
2 51553 West -11984.40
3 43110 Prarie -11861.46
4 22917 Atlantic -11769.17

Write and read Excel workbooks#

Problem: one file with several sheets, for spreadsheet users.

xl_path = "output/pandas/l07_report.xlsx"
with pd.ExcelWriter(xl_path) as writer:
    top.to_excel(writer, sheet_name="Top sales", index=False)
    losses.to_excel(writer, sheet_name="Losses", index=False)
print("saved", os.path.getsize(xl_path), "bytes")
saved 6443 bytes
book = pd.read_excel(xl_path, sheet_name=None)
for name, sheet in book.items():
    print(name, sheet.shape)
Top sales (5, 6)
Losses (5, 6)
lost = pd.read_excel(xl_path, sheet_name="Losses")
lost[["Order ID", "Order Date", "Profit"]].head(3)
Order ID Order Date Profit
0 37095 2009-03-10 -14140.7
1 43044 2009-10-17 -12558.0
2 51553 2011-11-27 -11984.4

Common mistake: asking for a sheet name that isn't there

pd.read_excel(xl_path, sheet_name="losses")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[18], line 1
----> 1 pd.read_excel(xl_path, sheet_name="losses")

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\excel\_base.py:494, in read_excel(io, sheet_name, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skiprows, nrows, na_values, keep_default_na, na_filter, verbose, parse_dates, date_format, thousands, decimal, comment, skipfooter, storage_options, dtype_backend, engine_kwargs)
    488     raise ValueError(
    489         "Engine should not be specified when passing "
    490         "an ExcelFile - ExcelFile already has the engine set"
    491     )
    493 try:
--> 494     data = io.parse(
    495         sheet_name=sheet_name,
    496         header=header,
    497         names=names,
    498         index_col=index_col,
    499         usecols=usecols,
    500         dtype=dtype,
    501         converters=converters,
    502         true_values=true_values,
    503         false_values=false_values,
    504         skiprows=skiprows,
    505         nrows=nrows,
    506         na_values=na_values,
    507         keep_default_na=keep_default_na,
    508         na_filter=na_filter,
    509         verbose=verbose,
    510         parse_dates=parse_dates,
    511         date_format=date_format,
    512         thousands=thousands,
    513         decimal=decimal,
    514         comment=comment,
    515         skipfooter=skipfooter,
    516         dtype_backend=dtype_backend,
    517     )
    518 finally:
    519     # make sure to close opened file handles
    520     if should_close:

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\excel\_base.py:1780, in ExcelFile.parse(self, sheet_name, header, names, index_col, usecols, converters, true_values, false_values, skiprows, nrows, na_values, parse_dates, date_format, thousands, comment, skipfooter, dtype_backend, **kwds)
   1630 def parse(
   1631     self,
   1632     sheet_name: str | int | list[int] | list[str] | None = 0,
   (...)
   1649     **kwds,
   1650 ) -> DataFrame | dict[str, DataFrame] | dict[int, DataFrame]:
   1651     """
   1652     Parse specified sheet(s) into a DataFrame.
   1653 
   (...)
   1778     >>> file.parse()  # doctest: +SKIP
   1779     """
-> 1780     return self._reader.parse(
   1781         sheet_name=sheet_name,
   1782         header=header,
   1783         names=names,
   1784         index_col=index_col,
   1785         usecols=usecols,
   1786         converters=converters,
   1787         true_values=true_values,
   1788         false_values=false_values,
   1789         skiprows=skiprows,
   1790         nrows=nrows,
   1791         na_values=na_values,
   1792         parse_dates=parse_dates,
   1793         date_format=date_format,
   1794         thousands=thousands,
   1795         comment=comment,
   1796         skipfooter=skipfooter,
   1797         dtype_backend=dtype_backend,
   1798         **kwds,
   1799     )

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\excel\_base.py:753, in BaseExcelReader.parse(self, sheet_name, header, names, index_col, usecols, dtype, true_values, false_values, skiprows, nrows, na_values, verbose, parse_dates, date_format, thousands, decimal, comment, skipfooter, dtype_backend, **kwds)
    750     print(f"Reading sheet {asheetname}")
    752 if isinstance(asheetname, str):
--> 753     sheet = self.get_sheet_by_name(asheetname)
    754 else:  # assume an integer if not a string
    755     sheet = self.get_sheet_by_index(asheetname)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\excel\_openpyxl.py:589, in OpenpyxlReader.get_sheet_by_name(self, name)
    588 def get_sheet_by_name(self, name: str):
--> 589     self.raise_if_bad_sheet_by_name(name)
    590     return self.book[name]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\excel\_base.py:605, in BaseExcelReader.raise_if_bad_sheet_by_name(self, name)
    603 def raise_if_bad_sheet_by_name(self, name: str) -> None:
    604     if name not in self.sheet_names:
--> 605         raise ValueError(f"Worksheet named '{name}' not found")

ValueError: Worksheet named 'losses' not found
print(pd.ExcelFile(xl_path).sheet_names)
pd.read_excel(xl_path, sheet_name="Losses").shape
['Top sales', 'Losses']
(5, 6)

Flatten JSON from an API#

Problem: web services send nested JSON, but pandas wants a flat table.

two = orders[["Order ID", "Region", "Sales"]].head(2)
print(two.to_json(orient="records", indent=2))
[
  {
    "Order ID":3,
    "Region":"Nunavut",
    "Sales":261.54
  },
  {
    "Order ID":293,
    "Region":"Nunavut",
    "Sales":10123.02
  }
]
json_path = "output/pandas/l07_orders.json"
orders.to_json(json_path, orient="records", date_format="iso")
from_json = pd.read_json(json_path)
print(from_json.shape)
(8399, 6)
response = {"status": "ok", "orders": [
    {"order_id": 3,
     "customer": {"name": "Muhammed MacIntyre",
                  "segment": "Small Business"},
     "items": [{"qty": 6, "sales": 261.54}]},
    {"order_id": 293,
     "customer": {"name": "Barry French", "segment": "Consumer"},
     "items": [{"qty": 49, "sales": 10123.02},
               {"qty": 27, "sales": 244.57}]},
]}
import json
api_path = "output/pandas/l07_api_response.json"
with open(api_path, "w") as f:
    json.dump(response, f, indent=2)
pd.read_json(api_path)
status orders
0 ok {'order_id': 3, 'customer': {'name': 'Muhammed...
1 ok {'order_id': 293, 'customer': {'name': 'Barry ...
with open(api_path) as f:
    data = json.load(f)
pd.json_normalize(data["orders"])
order_id items customer.name customer.segment
0 3 [{'qty': 6, 'sales': 261.54}] Muhammed MacIntyre Small Business
1 293 [{'qty': 49, 'sales': 10123.02}, {'qty': 27, '... Barry French Consumer

Quick check: how can we get one row per item, with its order ID?

items = pd.json_normalize(data["orders"], record_path="items",
                          meta=["order_id", ["customer", "name"]])
items
qty sales order_id customer.name
0 6 261.54 3 Muhammed MacIntyre
1 49 10123.02 293 Barry French
2 27 244.57 293 Barry French

Query a SQL database into pandas#

Problem: company data lives in databases, and SQL fetches it.

import sqlite3
conn = sqlite3.connect(":memory:")
rows = orders.to_sql("orders", conn, index=False)
print("rows written:", rows)
rows written: 8399
query = """
SELECT Region, ROUND(SUM(Sales)) AS total_sales
FROM orders
GROUP BY Region
ORDER BY total_sales DESC
"""
pd.read_sql(query, conn)
Region total_sales
0 West 3597549.0
1 Ontario 3063212.0
2 Prarie 2837305.0
3 Atlantic 2014248.0
4 Quebec 1510195.0
5 Yukon 975867.0
6 Northwest Territories 800847.0
7 Nunavut 116376.0
q = ("SELECT COUNT(*) AS n, AVG(Profit) AS avg_profit "
     "FROM orders WHERE Region = ?")
pd.read_sql(q, conn, params=("Ontario",)).round(2)
n avg_profit
0 1826 189.96

Common mistake: a column name with a space, left unquoted

pd.read_sql("SELECT Customer Segment FROM orders LIMIT 3", conn)
---------------------------------------------------------------------------
OperationalError                          Traceback (most recent call last)
File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\sql.py:2745, in SQLiteDatabase.execute(self, sql, params)
   2744 try:
-> 2745     cur.execute(sql, *args)
   2746     return cur

OperationalError: no such column: Customer

The above exception was the direct cause of the following exception:

DatabaseError                             Traceback (most recent call last)
Cell In[29], line 1
----> 1 pd.read_sql("SELECT Customer Segment FROM orders LIMIT 3", conn)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\sql.py:702, in read_sql(sql, con, index_col, coerce_float, params, parse_dates, columns, chunksize, dtype_backend, dtype)
    700 with pandasSQL_builder(con) as pandas_sql:
    701     if isinstance(pandas_sql, SQLiteDatabase):
--> 702         return pandas_sql.read_query(
    703             sql,
    704             index_col=index_col,
    705             params=params,
    706             coerce_float=coerce_float,
    707             parse_dates=parse_dates,
    708             chunksize=chunksize,
    709             dtype_backend=dtype_backend,
    710             dtype=dtype,
    711         )
    713     try:
    714         _is_table_name = pandas_sql.has_table(sql)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\sql.py:2809, in SQLiteDatabase.read_query(self, sql, index_col, coerce_float, parse_dates, params, chunksize, dtype, dtype_backend)
   2798 def read_query(
   2799     self,
   2800     sql,
   (...)
   2807     dtype_backend: DtypeBackend | Literal["numpy"] = "numpy",
   2808 ) -> DataFrame | Iterator[DataFrame]:
-> 2809     cursor = self.execute(sql, params)
   2810     columns = [col_desc[0] for col_desc in cursor.description]
   2812     if chunksize is not None:

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\sql.py:2757, in SQLiteDatabase.execute(self, sql, params)
   2754     raise ex from inner_exc
   2756 ex = DatabaseError(f"Execution failed on sql '{sql}': {exc}")
-> 2757 raise ex from exc

DatabaseError: Execution failed on sql 'SELECT Customer Segment FROM orders LIMIT 3': no such column: Customer
fixed = 'SELECT "Customer Segment" FROM orders LIMIT 3'
pd.read_sql(fixed, conn)
Customer Segment
0 Small Business
1 Consumer
2 Consumer

Your turn#

Use SQL to count the orders in each customer segment, largest first.

seg = """
SELECT "Customer Segment" AS segment, COUNT(*) AS n_orders
FROM orders GROUP BY segment ORDER BY n_orders DESC"""
pd.read_sql(seg, conn)
segment n_orders
0 Corporate 3076
1 Home Office 2032
2 Consumer 1649
3 Small Business 1642

Fix file encoding errors#

Problem: some files are saved in older encodings, like Latin-1.

names = sales[["Product Name"]].head(3)
old_path = "output/pandas/l07_latin1.csv"
names.to_csv(old_path, index=False, encoding="latin-1")
names
Product Name
0 Eldon Base for stackable storage shelf, platinum
1 1.7 Cubic Foot Compact "Cube" Office Refrigera...
2 Cardinal Slant-D¨ Ring Binder, Heavy Gauge Vinyl

Common mistake: opening a Latin-1 file with the default UTF-8

pd.read_csv(old_path)
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
Cell In[33], line 1
----> 1 pd.read_csv(old_path)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:872, in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, skip_blank_lines, parse_dates, date_format, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, on_bad_lines, low_memory, memory_map, float_precision, storage_options, dtype_backend)
    860 kwds_defaults = _refine_defaults_read(
    861     dialect,
    862     delimiter,
   (...)
    868     dtype_backend=dtype_backend,
    869 )
    870 kwds.update(kwds_defaults)
--> 872 return _read(filepath_or_buffer, kwds)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:300, in _read(filepath_or_buffer, kwds)
    297 _validate_names(kwds.get("names", None))
    299 # Create the parser.
--> 300 parser = TextFileReader(filepath_or_buffer, **kwds)
    302 if chunksize or iterator:
    303     return parser

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:1643, in TextFileReader.__init__(self, f, engine, **kwds)
   1640     self.options["has_index_names"] = kwds["has_index_names"]
   1642 self.handles: IOHandles | None = None
-> 1643 self._engine = self._make_engine(f, self.engine)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:1925, in TextFileReader._make_engine(self, f, engine)
   1922     raise ValueError(msg)
   1924 try:
-> 1925     return mapping[engine](f, **self.options)
   1926 except Exception:
   1927     if self.handles is not None:

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\c_parser_wrapper.py:95, in CParserWrapper.__init__(self, src, **kwds)
     92 if kwds["dtype_backend"] == "pyarrow":
     93     # Fail here loudly instead of in cython after reading
     94     import_optional_dependency("pyarrow")
---> 95 self._reader = parsers.TextReader(src, **kwds)
     97 self.unnamed_cols = self._reader.unnamed_cols
     99 passed_names = self.names is None

File pandas/_libs/parsers.pyx:568, in pandas._libs.parsers.TextReader.__cinit__()

File pandas/_libs/parsers.pyx:657, in pandas._libs.parsers.TextReader._get_header()

File pandas/_libs/parsers.pyx:868, in pandas._libs.parsers.TextReader._tokenize_rows()

File pandas/_libs/parsers.pyx:885, in pandas._libs.parsers.TextReader._check_tokenize_status()

File pandas/_libs/parsers.pyx:2082, in pandas._libs.parsers.raise_parser_error()

File <frozen codecs>:322, in decode(self, input, final)

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa8 in position 139: invalid start byte
fixed_names = pd.read_csv(old_path, encoding="latin-1")
print(fixed_names.loc[2, "Product Name"])
Cardinal Slant-D¨ Ring Binder, Heavy Gauge Vinyl

Mini-project: a regional profit report#

Question: which region earns the least profit per dollar sold?

Plan: CSV in, SQL summary, Excel and JSON out, verify, conclude.

report_sql = """
SELECT Region, COUNT(*) AS orders,
       ROUND(SUM(Sales)) AS sales,
       ROUND(SUM(Profit)) AS profit,
       ROUND(100 * SUM(Profit) / SUM(Sales), 1) AS margin_pct
FROM orders GROUP BY Region ORDER BY margin_pct
"""
report = pd.read_sql(report_sql, conn)
report
Region orders sales profit margin_pct
0 Nunavut 79 116376.0 2841.0 2.4
1 Yukon 542 975867.0 73849.0 7.6
2 West 1991 3597549.0 297009.0 8.3
3 Quebec 781 1510195.0 140427.0 9.3
4 Ontario 1826 3063212.0 346869.0 11.3
5 Prarie 1706 2837305.0 321160.0 11.3
6 Atlantic 1080 2014248.0 238961.0 11.9
7 Northwest Territories 394 800847.0 100653.0 12.6
out_xl = "output/pandas/l07_region_report.xlsx"
report.to_excel(out_xl, sheet_name="Regions", index=False)
report.to_json("output/pandas/l07_region_report.json",
               orient="records")
conn.close()
check = pd.read_excel(out_xl, sheet_name="Regions")
print(check.dtypes)
print((check == report).all().all())
Region            str
orders          int64
sales           int64
profit          int64
margin_pct    float64
dtype: object
True
import matplotlib.pyplot as plt
ax = check.plot(kind="barh", x="Region", y="margin_pct",
                legend=False, figsize=(7, 4))
ax.set_title("Profit margin by region")
ax.set_xlabel("Profit as % of sales")
ax.set_ylabel("Region")
plt.show()
No description has been provided for this image
worst = check.iloc[0]
best = check.iloc[-1]
print(f"Weakest: {worst.Region} at {worst.margin_pct}% margin")
print(f"Strongest: {best.Region} at {best.margin_pct}% margin")
Weakest: Nunavut at 2.4% margin
Strongest: Northwest Territories at 12.6% margin

Recap#

  • read_csv: usecols, parse_dates and nrows load exactly what you need
  • to_csv(index=False), then always read the file back to check
  • ExcelWriter writes many sheets; sheet_name=None reads them all
  • json_normalize flattens nested API data; read_sql runs SQL
  • A UnicodeDecodeError means: pass the right encoding

Challenges#

  1. Load only Province and Profit, then save the 10 biggest profits to Excel.
  2. Add a third sheet with the orders from Nunavut, and read all sheets back.
  3. Use SQL with a ? placeholder to find the total sales of one province.

What's next#

Lesson 08: Selecting Rows, Columns, and Subsets of Data Pick exactly the rows and columns you need with [], loc and iloc.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.