, ,

Python for Data Work, Past Automation Scripting (Infra to Data Science Series, Part 5)

Your automation scripts already move data. This part turns that habit into data code: load a month of cluster metrics with pandas, catch the dtype trap that silently breaks a metric column, and see why iterrows is slower than the plain loop you already write.

Infra to Data Science Series · Part 5 of 26

Two lines from a timing run I did while writing this part, on the same one month of cluster metrics: the pandas loop most people reach for first, iterrows, took 12,207 milliseconds to average one column, and the vectorised version took 21. Same answer, same machine, 581 times apart, and the slow one is the version most tutorials teach an operator to write.

Key takeaways: Your automation scripts already read, filter and summarise data, so the shift to data code is mostly unlearning the row by row loop. On 345,600 rows of cluster metrics a vectorised average ran in 21 milliseconds while iterrows took 12,207, which makes iterrows the one pandas habit to drop first, not adopt. read_csv guesses wrong when a collector writes a sentinel like a bare dash for a failed scrape, one column silently becomes text, and every later mean breaks. A reference map below turns each shell reflex, awk sums, grep filters, sort and uniq counts, into its one line pandas equivalent.
Who this is for: An infrastructure engineer, SRE or platform admin who writes Python for automation and bash and awk for glue, and wants to turn that into data code. You know loops, files and the command line. You have not yet worked in pandas at any depth. Terms on first use: a dataframe is a table held in memory with typed columns; vectorised means an operation runs over a whole column in compiled C at once instead of one Python row at a time; a dtype is the stored type of a column, such as float64 or object; iterrows is a pandas method that walks a frame one row at a time.

From awk habits to dataframe habits

You have been doing data engineering for years without the title. Every log parser, every capacity report piped through awk, every for loop that walks a metrics file and sums a field is data work already. What changes in data science is not the goal but the shape of the code. An operator instinct is imperative and row oriented: open the file, walk it line by line, split each line, accumulate into a dict. That instinct is correct for a one off on a 200 line log. It falls apart at 345,600 rows, and it falls apart harder the moment you want to join, group, resample or model, because you end up rebuilding pandas by hand and badly.

Data code is column oriented and declarative. You load the whole table once, then describe the transformation you want, average this column, group by that key, and let the library run it in compiled code. Same result, a tenth of the lines, and orders of magnitude faster on anything real. This jump is not syntax memorisation, it is a change of default from loop over rows to operate on columns. Below is the translation guide, and it is the artifact worth keeping from this part, because most shell moves you already make have a one line pandas twin.

Operator reflexWhat it doesData code equivalent
for line in open(f)read and parse rowspd.read_csv(path)
awk sum of a fieldtotal one columndf[col].sum()
grep patternkeep matching rowsdf[df[col] == value]
sort then uniq minus ccount by valuedf[col].value_counts()
cut a fieldpick a columndf[col]
loop building per key totalsgroup and aggregatedf.groupby(key)[col].mean()
date math in a loopbucket by timeparse_dates then resample(1D)

Why a column operation runs so much faster, contiguous memory and one compiled loop instead of many Python ones, is covered in the Data Science Series part on NumPy and pandas. Here it is enough to trust that describing the column beats walking the rows, and to build the habit on your own data.

A month of metrics, loaded honestly

Last part we named the export to pull, a month of one cluster CPU and memory metrics. This part it lands in Python. My copy is 345,600 rows, eight hosts sampled every minute through June, two metric columns and a timestamp. Yours will look different in the values and identical in the failure modes. A naive load looks like one line and mostly works, but look at the dtypes before you trust a single number, because that is exactly where the load lies to you.

flowchart LR
  A[Raw export CSV] --> B[read_csv, declare na_values and parse_dates]
  B --> C{dtypes correct}
  C -->|No| D[find the sentinel, coerce to numeric]
  C -->|Yes| E[Vectorised groupby and resample]
  D --> E
  E --> F[Tidy frame ready to model]
Load and clean for one export. Every step describes a column operation, and not one of them loops over rows.
# tested on Python 3.10, pandas 2.3.3, numpy 2.2.6; runs unchanged on pandas 2.2
import pandas as pd

df = pd.read_csv('cluster_metrics.csv')
print(df.shape)
print(df.dtypes)
# the average an operator expects
print(df['cpu_pct'].mean())
(345600, 4)
timestamp    object
host         object
cpu_pct      object
mem_mb        int64
dtype: object
Traceback (most recent call last):
  ...
TypeError: Could not convert string '20.3029.2022.4037.95...' to numeric
Gotcha: read_csv already knows the common missing value tokens, empty string, NA, NaN, null, even N slash A, and turns them into real missing values. It does not know your collector private conventions. Mine writes a bare dash when a scrape fails, 1,071 times across the month. pandas sees a column of numbers with dashes sprinkled through it, decides the only safe common type is text, and hands back cpu_pct as object dtype. Each value still looks like a number on screen, so the bug hides until you call mean and get a TypeError instead of a percentage.
df = pd.read_csv(
    'cluster_metrics.csv',
    na_values=['-'],
    parse_dates=['timestamp'],
)
print(df.dtypes)
print('missing cpu rows:', int(df['cpu_pct'].isna().sum()))
print('cpu mean:', round(df['cpu_pct'].mean(), 2))
timestamp    datetime64[ns]
host                 object
cpu_pct             float64
mem_mb                int64
dtype: object
missing cpu rows: 1071
cpu mean: 25.39

Two arguments fix it. na_values tells read_csv that a dash means missing, and parse_dates turns the timestamp text into real datetimes so you can bucket by day later. Now cpu_pct is float64, the 1,071 bad scrapes are proper missing values that mean quietly skips, and the average is a believable 25.39 percent instead of an exception. Run this dtype check on every export before you compute anything, because a metric column that came in as text is the single most common silent break in a beginner analysis.

Vectorisation over iteration, with real timings

With the data honest, here is the habit that matters most. My task is a per host average CPU, the kind of group and summarise you have written in awk a hundred times. There are three ways to do it: the pure Python loop you already know, the pandas iterrows loop a tutorial will show you, and the vectorised groupby. I timed all three on the real 345,600 rows, and the ranking is not what a newcomer expects.

import csv

def per_host_scripting():
    sums, counts = {}, {}
    with open('cluster_metrics.csv') as f:
        r = csv.reader(f); next(r)
        for ts, host, cpu, mem in r:
            if cpu == '-':
                continue
            sums[host] = sums.get(host, 0) + float(cpu)
            counts[host] = counts.get(host, 0) + 1
    return {h: sums[h] / counts[h] for h in sums}

def per_host_iterrows(df):
    sums, counts = {}, {}
    for _, row in df.iterrows():
        c = row['cpu_pct']
        if pd.isna(c):
            continue
        h = row['host']
        sums[h] = sums.get(h, 0) + c
        counts[h] = counts.get(h, 0) + 1
    return {h: sums[h] / counts[h] for h in sums}

def per_host_vectorised(df):
    return df.groupby('host')['cpu_pct'].mean().to_dict()
scripting loop :    252.0 ms
iterrows       :  12206.8 ms
vectorised     :     21.0 ms
speedup vectorised vs iterrows: 581x
One column averaged, three waysmilliseconds, log scale, 345,600 rows, lower is fastervectorised21 msscripting loop252 msiterrows12,207 msiterrows runs 48x slower than the plain loop and 581x slower than vectorised
Same aggregation, three implementations, log scale because the bars span three orders of magnitude. The method named for iteration is the slowest thing on the chart.

Read the middle line twice. iterrows, whose name promises it is the pandas way to loop, is the slowest by a wide margin, 48 times slower than the plain file loop it was meant to improve on. It is slow because it builds a fresh Series object for every single row and casts each value to a Python object on the way, so you pay a heavy tax 345,600 times. So the tutorial default is the trap here, not the fix. If you ever truly must loop a frame, itertuples is far faster than iterrows because it skips that per row Series, but the honest advice is simpler: describe the column and do not loop at all. A vectorised groupby came out 581 times faster than iterrows and, tellingly, 12 times faster than the bash style loop you started from. That same instinct, prefer a compiled batch path over a per item Python loop, is what pays off in language model latency work too, where batching and caching beat per request handling for the identical reason.

A reusable cleaning pipeline

Put the pieces into one small recipe you can run against any metrics export. Load with the sentinel declared and the dates parsed, add a derived column with assign rather than a loop, then aggregate. Here it produces a daily peak CPU per host, 240 rows out of 345,600, which is the compact shape a capacity model actually wants to see.

def load_metrics(path):
    return (
        pd.read_csv(path, na_values=['-'], parse_dates=['timestamp'])
          .assign(mem_gb=lambda d: d['mem_mb'] / 1024)
    )

df = load_metrics('cluster_metrics.csv')

daily_peak = (
    df.set_index('timestamp')
      .groupby('host')['cpu_pct']
      .resample('1D')
      .max()
      .rename('daily_peak')
      .reset_index()
)
print(daily_peak.shape)
print(daily_peak.head(3).to_string(index=False))
(240, 3)
   host  timestamp  daily_peak
node-00 2026-06-01       45.52
node-00 2026-06-02       44.91
node-00 2026-06-03       32.42

Notice there is not a single for loop in that recipe. set_index, groupby, resample and assign each describe an operation over columns, and pandas runs them in compiled code. That is the whole mindset shift on one screen. Getting the data in, from files today and from SQL, APIs and monitoring next, is its own subject, and the Data Science Series walks the connection patterns in getting data into Python. For now you have a repeatable load that any export can flow through.

War story: I once replaced a capacity report that was a bash and awk one liner with a proper pandas rewrite, and to feel thorough I looped the frame with iterrows to compute per host peaks. On the full fleet, roughly 20 million rows for a quarter, the job that used to finish in seconds took nine minutes and blew past its cron window, which is how I found out, from a pager alert about an overrunning job. I nearly filed it as pandas being slower than awk. Then I deleted the loop and wrote one groupby with a resample, and the same report finished in under ten seconds. That evening cost me a bruised assumption and reversed my conclusion: pandas was never the slow part, iterrows was.

Project status and the recipe to keep

Where the project stands now: one month of raw metrics is loaded, typed correctly, its failed scrapes marked missing rather than silently poisoning the math, and reduced to a tidy daily frame. That is phase two of the roadmap from Part 4 starting exactly on schedule. Your reference artifact from this part is really two things, the reflex map table for translating shell moves into pandas and the short load recipe that declares the sentinel and parses dates so nothing lies to you downstream. Paste both into your project notes today, because Parts 6 through 9 build straight on this loaded frame rather than starting over.

One honest caveat before the next part. All of this ran comfortably in memory because 345,600 rows is small. A year of second by second metrics across a fleet is not, and at that size the same read_csv call will exhaust RAM and fall over. That is a real limit with real fixes, chunked reads and pruning to only the columns you need, that the memory half of the NumPy and pandas material covers. Note it now, and reach for those tools the day your export stops fitting, not before.

Load the export before you write another loop

If you do one thing after reading this, open a notebook and run read_csv on your own export, then immediately print dtypes and scan for any column that should be a number and came back as object. That one check catches the failure that silently breaks more beginner analyses than any other. When a column is wrong, find the sentinel your collector writes, pass it to na_values, parse your timestamp, and confirm the mean is a number you believe. Do not write another parsing loop, because you already have one that works and the point of this part is that you rarely need it again. Next part takes this same loaded frame and makes it reproducible, the version control and environment discipline you already practise in ops, applied to data work. Load your export first, then meet it there.

Infra to Data Science Series · Part 5 of 26
« Previous: Part 4  |  Guide  |  Next: Part 6 »

References

About The Author


Discover more from Journal of Intelligent Infrastructure

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure

Subscribe now to keep reading and get access to the full archive.

Continue reading