> ## Content Index
> Fetch the complete content index at: https://www.bambooweekly.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Bamboo Weekly #26: Hot weather (solutions)
- URL: https://www.bambooweekly.com/bw-26-hot-weather-solution/
- Published: 2023-07-27T15:00:16.000Z
- Updated: 2026-08-23T09:37:09.000Z
- Description: Get practice with fixed-width fields, multiple files, comprehensions, memory optimization, joining, and plotting
- Author: Reuven M. Lerner
- Tags: fixed-width-fields, multiple-files, comprehensions, memory-optimization, joins, plotting

This week, we looked at high-temperature data, in an attempt to see where and when it has been particularly hot over the years, and whether we can see a general trend toward hotter temperatures.

Our data this week came from the National Centers for Environmental Information ([NCEI](https://www.ncei.noaa.gov/?ref=bambooweekly.com)), part of the National Oceanic and Atmospheric Administration ([NOAA](https://www.noaa.gov/?ref=bambooweekly.com)), part of the US Department of Commerce.

The data is *huge*, even after we whittle it down and use only a part of it. Part of this week’s learning goals to include taking a large number of files and turning them into a data frame containing only some of the data.

I first suggested that you look through the data dictionary for this data set, to understand the structure and contents of what we’re going to be working with. The data dictionary is located here:

[https://www.ncei.noaa.gov/pub/data/ghcn/daily/readme.txt](https://www.ncei.noaa.gov/pub/data/ghcn/daily/readme.txt?ref=bambooweekly.com)

I then gave you 7 questions and tasks for this week, the bulk of the work being in question 3, where we create the data frame based on the files.

So, without further ado, let’s get to this week’s questions:

### Download the list of weather stations ([https://www.ncei.noaa.gov/pub/data/ghcn/daily/ghcnd-stations.txt](https://www.ncei.noaa.gov/pub/data/ghcn/daily/ghcnd-stations.txt?ref=bambooweekly.com)) and turn it into a data frame. Use the specifications for the file, as described in the README. You'll want to set your own names for the column headers. Make the \`id\` column into the index.

There are a lot of weather stations positioned all over the world, and the data set that we’re working with this week includes data from all of them. Each weather station has a unique ID, as well as a location name, longitude, and latitude. Turning the stations into a data frame is a good first step.

Before doing anything else, I decided to load up NumPy and Pandas:

```
import numpy as np
import pandas as pd
from pandas import Series, DataFrame
```

With those in place, I then started to work on the data itself, create a new data frame with the station information.

Most of the time, we deal with files in CSV or Excel format. However, the stations aren’t in either of those formats, as you might have seen in the data dictionary. Instead, they are in “fixed-width format,” meaning that while each line in the file contains a single record, the fields aren’t separated by delimiter characters. Rather, each field contains a specific number of characters.

Fortunately, Pandas comes with [read\_fwf](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read%5Ffwf.html?ref=bambooweekly.com), a method for reading from fixed-width field files. If you read through the README file, you’ll see that section IV indicates that each line contains 85 characters, divided into nine fields:

```
------------------------------
Variable   Columns   Type
------------------------------
ID            1-11   Character
LATITUDE     13-20   Real
LONGITUDE    22-30   Real
ELEVATION    32-37   Real
STATE        39-40   Character
NAME         42-71   Character
GSN FLAG     73-75   Character
HCN/CRN FLAG 77-79   Character
WMO ID       81-85   Character
------------------------------
```

In theory, we could call read\_fwf without any arguments other than the filename from which we want to read. But in reality, that’ll give us a huge mess. That’s because by default, read\_fwf tries to infer where the columns are. In a file like this one, where there are numerous empty fields, it’ll quickly get confused and give us the wrong number of fields.

Fortunately, the data dictionary tells us the columns used by each field. Which means that we can indicate which field goes where by passing a list of tuples to the “colspecs” argument:

```
stations_df = pd.read_fwf(stations_filename, 
                 colspecs=[(0,11), (12, 20), (21,30), (31,37), (38,40), (41,71), (72, 75), (76,79), (80,85)]) 
```

First of all, notice that the numbers in my tuples and the numbers in the above specification from the README aren’t quite the same. That’s because the data dictionary called the first column 1 — but in Python, the first column is 0\. We thus need to subtract 1 from the starting point for each of the columns.

What about the ending point? Shouldn’t the first tuple be (0, 10) rather than the specified (1, 11)? No, because Python (and read\_fwf) almost always assume “up to but not including” the endpoint. So when we say (0, 11), we mean that we want 11 characters, starting with index 0, and going through index 10.

I’m not sure why, but the format used for the stations leaves an empty character between columns, which just makes it more confusing.

If you use the above code to read in the file, you’ll quickly discover that it’s still a bit weird. That’s because read\_fwf, like read\_csv, assumes that the first row names the columns. To convince it otherwise, we’ll need to set “header=None”, indicating that there are no headers in this file, and then pass a list of strings to the “names” argument, telling it what to call the columns.

Finally, we can pass “index\_col” and name the “id” column (defined in “names” above) as our index:

```
stations_df = pd.read_fwf(stations_filename, 
                 header=None, 
                 names='id latitude longitude elevation state name gsn_flag hcn_crn_flag wmo_id'.split(), 
                 colspecs=[(0,11), (12, 20), (21,30), (31,37), (38,40), (41,71), (72, 75), (76,79), (80,85)] ,
                 index_col='id') 
```

The result is a data frame with 124,954 rows, each describing a different weather-monitoring station somewhere in the world.

### Download the GHCND-ALL data ([https://www.ncei.noaa.gov/pub/data/ghcn/daily/ghcnd\_all.tar.gz](https://www.ncei.noaa.gov/pub/data/ghcn/daily/ghcnd%5Fall.tar.gz?ref=bambooweekly.com)). NOTE: This file is 3.4 GB in size, so it might take a while to download to your computer. Follow the directions in the README to un-tar the file. This will result in about 30 GB of files being created under the \`ghcnd\_all\` directory.

Most people are used to working with zipfiles. A zipfile can contain a number of files within it, and also compresses those files. It’s thus super convenient to work with zip.

But before zip came along, there was “tar” format, short for “tape archive.” The idea of a tarfile was that you could take a whole bunch of files and put them together inside of one file, typically for backup purposes. I could “tar up” an entire directory, including its files and subdirectories, and store that file somewhere. Then, if/when I need to retrieve those files, I could untar them.

Note that tar archived files, but didn’t compress them. A few different compression schemes existed at the time, but the “GNU zip” format (aka “gzip”), from the people at the Free Software Foundation (sort of a predecessor to the open-source movement), quickly took hold. Note that there was no connection between gzip and the zip that we now know; I’d argue that this was a foolish choice on the GNU people’s part, but there you have it.

In the Unix world, it’s thus pretty common to have a file with the “.tar.gz” dual suffix. To open the file, you first need to un-gzip it, and then you have to un-tar it. There are far too many options to both tar and gzip to explain them here, but the README explains that after you have downloaded the ghcnd\_all.tar.gz file, you can open it up into a directory with the following command:

```
tar xzvf ghcnd_all.tar.gz
```

In short, the above command first un-gzips the file (that’s the “z” option), then extracts the file (that’s the “x” option), doing it verbosely (i.e., telling us what it’s doing) and working on the file we specify (i.e., ghcnd\_all.tar.gz).

The gzipped tarfile that we downloaded is 3.4 GB in size. And when opened up, into a new directory? The complete contents are 30 GB, with 124,946 files, each ending with “.dly”, meaning that it contains daily information from a particular weather station.

### Create a data frame with just the TMAX readings, from all stations starting in 1990\. Only store the station ID, year, month, date, and the reading value.

This part of things was … challenging, for at least two reasons: First, there were a lot of files, adding up to a large amount of data. Second, we needed to transform the data from each of the “dly” files into something that could be used as a data frame.

I decided to do this in three stages:

1. First, I would iterate over every line, of every file, keeping only those lines with a maximum-temperature reading (i.e., TMAX). The result would be a list of strings.
2. Then, I would iterate over that list of strings, filtering out the records that we don’t want or need, and returning a dict for each record that we do keep.
3. Create a data frame based on the list of dicts.

So, let’s step through this, one piece at a time.

First: Each of the 124,946 “.dly” files has a common format:

1. Each file is from a single weather station. The filename (without the extension) is the weather station’s code, now the index in “stations\_df”.
2. Each line in a file contains the measurements of one weather statistic for one month. If we have 12 months and 10 statistics, then there will be 120 lines in the file.
3. As indicated in the README, these files (like the station file) use fixed-width fields. The first field contains:

  1. the station ID
  2. the 4-digit year
  3. the 2-digit month
  4. a four-character code indicating what measurement we’re taking

For example, here’s the first field from one file:

```
ACW00011604194901TMAX
```

This is from weather station ACW00011604, January 1949, measuring max temperature.

Then, for each day of the month, there will be four pieces of additional information:

1. The measured value
2. Measurement flag (when was it measured?)
3. Were there quality problems?
4. What is the source?

We’re going to ignore all but the first value.

For each of the 31 potential days in a month, all four of these appear; each line will contain exactly 269 characters.

Missing data is indicated by having a value of -9999.

So, how can I create a list of strings based on the TMAX lines in every “.dly” file in this directory? I decided to use a nested list comprehension, iterating over every line in every file that I can find with the standard library’s “[glob](https://docs.python.org/3/library/glob.html?ref=bambooweekly.com)” module:

```
import glob

all_tmax = [one_line.removesuffix('\n')
 for one_filename in glob.glob('ghcnd_all/*.dly')
 for one_line in open(one_filename)
 if one_line.split()[0].endswith('TMAX')
 if int(one_line[11:15]) > 1990]]
```

What does this do? Let’s take it apart, bit by bit:

1. I use glob.glob to return a list of all files matching the pattern “ghcnd\_all/\*.dly”, or the daily files.
2. I then iterate over each of the filenames in that list, assigning each filename in turn to the variable one\_filename.
3. I then go through each of the files, one line at a time, assigning each line to the variable “one\_line”.
4. I split the line on whitespace, checking whether the first element of that list ends with the string TMAX.
5. I also grab the year from the string, and check whether it’s greater than 1990.
6. If both of these conditions are true (i.e., if it’s a TMAX reading from after 1990), we add the string (minus its newline) to the output.

When we’re done with this process, we have a list of 5,944,767 269-character strings. Each string represents one month’s max temperature readings from one weather station.

Here’s what all\_tmax\[:3\] looks like on my computer:

```
['AM000037959199004TMAX   72  S-9999      60  S-9999   -9999   -9999   -9999     181  S-9999   -9999   -9999   -9999     205  S  200  S  126  S-9999   -9999   -9999   -9999     220  S  141  S-9999     209  S  212  S  210  S-9999     266  S-9999     189  S-9999   -9999   ',
 'AM000037959199006TMAX  312  S-9999     336  S-9999   -9999   -9999     282  S  322  S-9999   -9999   -9999     310  S  322  S-9999   -9999     232  S-9999     238  S-9999     205  S  250  S-9999   -9999     303  S  285  S-9999   -9999   -9999   -9999     248  S-9999   ',
 'AM000037959199011TMAX  202  S-9999   -9999   -9999   -9999     255  S-9999     150  S-9999     125  S-9999   -9999     104  S-9999   -9999     122  S-9999     180  S  202  S-9999     180  S  174  S-9999   -9999   -9999   -9999   -9999   -9999     188  S-9999   -9999   ']
```

Next, I want to turn this list of strings into a list of dicts. It would almost certainly use less memory to use a list of lists, but I find dicts easier to think about and work with, and decided that the memory hit wouldn’t be too bad.

I decided to perform this transformation using a standard “for” loop, appending to a list. I could also have done it using a combination of a function and a list comprehension — and yes, I could have done it in a single pass with the above list comprehension and a function — but I often prefer to do things in stages, so that I can see what’s happening and debug it.

(For example, an earlier version of the above code used “strip” to remove the newline at the end. That had the side effect of removing trailing spaces. I also decided to use a second “if” statement in the above list comprehension to filter by year, rather than doing it in my dict-building “for” loop. By splitting things into pieces, I was able to debug, refactor, and try different approaches.)

Here’s the code I came up with to create a list of dicts with all records for my data frame:

```
all_records = []

for index, one_record in enumerate(all_tmax):
    if index % 1_000_000 == 0:
        print(f'{index:12,} -- ({(index/len(all_tmax)) * 100:2.2f}% done)')
    station_id = one_record[:11]
    year = int(one_record[11:15])
    month = int(one_record[15:17])
    element = one_record[17:21]

    for day_number, day_start in enumerate(range(21,269,8), 1):
        value = one_record[day_start:day_start+5]

        if value and float(value) == -9999:
            continue   # ignore NaN values

        measurement_flag = one_record[day_start+5:day_start+6]
        quality_flag = one_record[day_start+6:day_start+7]
        source_flag = one_record[day_start+7:day_start+8]

        all_records.append(
             {'station_id':station_id,
              'year':year,
              'month':month,
              'day':day_number,
              'value':float(value) / 10})  # tenths of degrees Celsius

print('Done')        
```

So, what’s happening here? Truth be told, not *that* much: We’re iterating over a list of strings, grabbing slices from each of those strings, and then turning them into a dict with specified keys and values.

The tricky thing is that each row of the file represents 31 different readings, one for each day in the month. Yes, even months without 31 days have 31 entries, with values of -9999 to indicate NaN. The value -9999 is also used to indicate that there was a problematic or missing reading. In all of these cases, we’ll just ignore values of -9999.

We’ll also ignore the measurement, quality, and source flags; if you have enough time and memory to include them in your data frame, then that’s great, and you can do tons of sophisticated analysis on max temperatures. But since we aren’t going to use them, I left them out of the output dict.

Notice that I used “[enumerate](https://docs.python.org/3/library/functions.html??ref=bambooweekly.com#enumerate)” in two places:

1. In the outer “for” loop, iterating over the strings in “all\_tmax”. This was just so that I could get a printout indicating how far along we were in processing the data. I often like to use this sort of loop and printout when I’m nervous about how long things are taking. I took advantage of the formatting we can do in f-strings to display the number of records we have processed, as well as the percentage we have finished, in a nice way. If you’re curious about f-string formatting, take a look at the handy guide at [https://fstring.help/](https://fstring.help/?ref=bambooweekly.com).
2. In the inner “for” loop, iterating over the indexes in the current string. Each day’s entry consumes 8 characters, and I used “enumerate” to count which day of the month we’re on. I passed a second, optional argument to “enumerate” so that it would start counting days with 1, not 0.

And yes, I used “[range](https://docs.python.org/3/library/stdtypes.html??ref=bambooweekly.com#range)” to get the start of the fields for each day of the month, starting with character 21 and going up to the end of the string, adding 8 with each iteration.

I ended up with a list of 174,668,969 dictionaries, each representing one TMAX reading from one day at one weather station.

This is what I get from requesting all\_records\[:3\]:

```
[{'station_id': 'AM000037959',
  'year': 1990,
  'month': 4,
  'day': 1,
  'value': 7.2},
 {'station_id': 'AM000037959',
  'year': 1990,
  'month': 4,
  'day': 3,
  'value': 6.0},
 {'station_id': 'AM000037959',
  'year': 1990,
  'month': 4,
  'day': 8,
  'value': 18.1}]
```

Notice that the temperature is measured in degrees Celsius. In my “for” loop, I took the value in the file and divided it by 10, since the README indicated that the stored values were in tenths of degrees Celsius.

With this list of dicts in place, I can now create a data frame:

```
df = DataFrame(all_records)
```

I know, this code is a bit of a let-down, after everything we’ve written before. But it works, and it works well!

I should note that each of these steps took a long time to run, even on my souped-up iMac with tons of RAM. (I’m told that Apple’s newer M1 and M2 chips run far faster than my machine, but I’m guessing that it’ll still take a while.)

### How much memory is the data currently using? Can you cut that down?

If we want to know how much memory a data frame is using, we can use the [memory\_usage](https://www.bambooweekly.com/pandas-memory-usage/) method on our data frame:

```
Index                132
station_id    1397351752
year          1397351752
month         1397351752
day           1397351752
value         1397351752
dtype: int64
```

This doesn’t mean much on its own, but because we got a series back, we can invoke “sum” on the numbers and get a total of 6,986,758,892\. In other words, our data frame is using 6.9 GB.

We can get a similar reading if we use the “[info](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.info.html?ref=bambooweekly.com)” method, which summarizes everything about a data frame:

```
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 174668969 entries, 0 to 174668968
Data columns (total 5 columns):
 #   Column      Dtype  
---  ------      -----  
 0   station_id  object 
 1   year        int64  
 2   month       int64  
 3   day         int64  
 4   value       float64
dtypes: float64(1), int64(3), object(1)
memory usage: 6.5+ GB
```

See the final line there, where it says how much memory we’re using? It says 6.5+ GB. What is the + doing there?

The thing is, Pandas isn’t really calculating all of the memory we’re using. Rather, it’s calculating the memory being used by the NumPy backend. That includes all of the numeric values, but it doesn’t include the strings — which are what we’re using for the “station\_id” column.

We can force Pandas to perform a real check, and calculate the lengths of the strings we’re using, by passing the memory\_usage='deep' keyword argument to info. This takes far longer to execute, but gives us a truly accurate count of how much memory is being used. The result:

```
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 174668969 entries, 0 to 174668968
Data columns (total 5 columns):
 #   Column      Dtype  
---  ------      -----  
 0   station_id  object 
 1   year        int64  
 2   month       int64  
 3   day         int64  
 4   value       float64
dtypes: float64(1), int64(3), object(1)
memory usage: 16.3 GB
```

See that? We’re actually using 16.3 GB of RAM. Which is indeed more than 6.5 GB, but … that first number was off by quite a bit.

How can we pare that memory usage down? By changing the dtypes we’re using to smaller values. (I’m guessing that we could also save memory by using PyArrow, but I decided not to overcomplicate things here.) Consider:

- We cannot really change the strings used by station\_id. We’re stuck with those, unless we switch to PyArrow.
- The year is an integer, but doesn’t need 64 bits. We can’t use 8 bits (since that’ll max out at 256), but we can definitely use 16.
- The month and day can both be 8-bit integers.
- The value can be a 16-bit float; we only care about tenths of degrees, anyway. (You could also argue that we could keep these values as integers, stored with their original values, and then use 8-bit integers for even bigger memory savings.)

Let’s change the dtypes of these columns:

```
df['year'] = df['year'].astype(np.int16)
df['month'] = df['month'].astype(np.int8)
df['day'] = df['day'].astype(np.int8)
df['value'] = df['value'].astype(np.float16)
```

How much memory are we using now?

```
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 174668969 entries, 0 to 174668968
Data columns (total 5 columns):
 #   Column      Dtype  
---  ------      -----  
 0   station_id  object 
 1   year        int16  
 2   month       int8   
 3   day         int8   
 4   value       float16
dtypes: float16(1), int16(1), int8(2), object(1)
memory usage: 12.0 GB
```

In other words, we’ve saved more than 4 GB of memory, with no reduction in the accuracy of our data, with just a few short commands. This is still a relatively large data set, but it’s far more manageable than would otherwise have been the case.

### What were the 20 highest temperatures ever recorded? What were the names of the weather stations where they were recorded?

Now that we have our data in place, we can start to conduct actual queries. First off, what were the 20 highest temperatures ever recorded? To find that, we’ll [sort our data by the “value” columns](https://www.bambooweekly.com/pandas-sort-values/), highest to lowest, and grab the top 20 rows with “[head](https://www.bambooweekly.com/pandas-head/)”:

```
df.sort_values('value', ascending=False).head(20)
```

This works just fine, but it gives us the IDs of the weather stations where those measurements were taken, not their names. Those names are back in the “stations\_df” data frame we started with. However, we can [set the station ID to be the index](https://www.bambooweekly.com/pandas-set-index/) of our data frame, and then [join](https://www.bambooweekly.com/pandas-join/) it with stations\_df, thus getting the names:

```
(
    df.sort_values('value', ascending=False)
    .head(20)
    .set_index('station_id')
    .join(stations_df)
)
```

Sure enough, I get all of the information I wanted:

![](https://storage.ghost.io/c/06/ba/06ba0cc0-be6f-4de7-af2f-5c20165279b9/content/images/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https-3a-2f-2fsubstack-post-media.s3.amazonaws.com-2fpublic-2fimages-2f4f4a19d2-d470-4909-b9bc-ae7b6860d4d4_3152x1896.png)

(And yes, it did occur to me that with the longitude and latitude, we could use GeoPandas to put this data on a map… but this is a weekly set of exercises, not a weekly book, so I had to put that thought aside.)

We can see that the three highest temperatures were outside of the United States (since the “state” column is NaN), and all (from the names) appear to be in China. I was surprised to see Alaska named as having a particularly high temperature. And indeed, all of these temperatures are suspiciously high. I’m wondering whether I messed up in my interpretation of the data (definitely possible!) or if the data was input in the wrong way. Or both. The fact that the temperatures are numbers like 3276 tells me that perhaps they should have been seen as 32.76 Celsius, a not-unreasonable reading — but not what I saw in the README.

I actually went back, after seeing these results, and added the quality flag to my data frame. I wanted to know if there was some consistently wrong or weird thing that I was missing. Sure enough, all of these values had a quality code of X, “failed bounds check.” So… the data was wrong, and as marked as such. And I guess I shouldn’t have told you to ignore the quality score!

Indeed, I found that 808,945 rows in our data frame had a quality code that wasn’t a single space character (i.e., had problems). I removed those rows:

```
df = df.loc[df['qflag'] == ' ']
```

Then I re-ran my query:

```
(
    df.sort_values('value', ascending=False)
    .head(20)
    .set_index('station_id')
    .join(stations_df)
)    
```

The result seems much more reasonable now:

![](https://storage.ghost.io/c/06/ba/06ba0cc0-be6f-4de7-af2f-5c20165279b9/content/images/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https-3a-2f-2fsubstack-post-media.s3.amazonaws.com-2fpublic-2fimages-2f6b293a80-5ceb-4ffe-afdc-378cdd6631a0_3176x1880.png)

I hadn’t heard of some of these places (e.g, Oued Irara is in Algeria, for example), but several are in places (Saudi Arabia and California) that are known to be super hot.

I’m not sure if all of the data is 100 percent reliable; when you hear that people in Beverly Hills are hot, temperature isn’t usually what you think about. But this definitely seems more reliable and in line with my expectations than the previous data.

### What 10 weather stations have, on average, had the highest recorded temperatures?

With the cleaned-up data in place, let’s now find out which weather stations have, on average, recorded the highest temperatures. First, my code:

```
(
    df.groupby('station_id')
    [['value']]
    .mean()
    .sort_values('value', ascending=False)
    .head(10)
    .join(stations_df)
)
```

In the above code, I first run a “[groupby](https://www.bambooweekly.com/pandas-groupby/)”, so that I’ll get one result per value of station\_id. The result will be from running “mean” on the “value” column — meaning, we’ll get the mean temperature recorded at each station.

Notice that I used \[\['value'\]\] here. I could have used just \['value'\], but that would have returned a series, and I wanted to join the result with a data frame — and a series doesn’t have the “join” method. I thus got a data frame back (by asking for the results on a list of columns, even though that list had a single element), sorted it by value, got the 10 highest-value rows, and then joined it with stations\_df:

![](https://storage.ghost.io/c/06/ba/06ba0cc0-be6f-4de7-af2f-5c20165279b9/content/images/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https-3a-2f-2fsubstack-post-media.s3.amazonaws.com-2fpublic-2fimages-2f978087d1-e0ae-465b-9d6e-353b363a62b6_2566x1044.png)

On average, then, the highest max temperatures are in Tihuatlan (Mexico) and In-Guezzam (Algeria).

### Plot the average global temperature for each month over the last 10 years. Do we see the values rising?

Finally, I asked you to plot average global temperatures for each month over the last 10 years.

First, we’ll need to calculate the mean high temp for each month. We can do that with “groupby”, grouping on both “year” and “month”, then using the “[plot](https://www.bambooweekly.com/pandas-plot/)” method:

```
df.groupby(['year', 'month'])['value'].mean().plot.line()
```

This produces the following plot:

![](https://storage.ghost.io/c/06/ba/06ba0cc0-be6f-4de7-af2f-5c20165279b9/content/images/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https-3a-2f-2fsubstack-post-media.s3.amazonaws.com-2fpublic-2fimages-2f6f9add44-6690-458e-a4af-6c62c3dc365b_550x432.jpg)

I don’t see much of a clear trend in the mean temperatures. But perhaps that’s the wrong measure; instead of looking at monthly averages, I can look at the annual maximums:

```
df.groupby('year')['value'].max().plot.line()
```

I don’t know about you, but I definitely see a trend that’s generally moving up and to the right here:

![](https://storage.ghost.io/c/06/ba/06ba0cc0-be6f-4de7-af2f-5c20165279b9/content/images/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https-3a-2f-2fsubstack-post-media.s3.amazonaws.com-2fpublic-2fimages-2f54181711-366f-461a-b18e-7d07155468d3_556x432.jpg)

Whew! I’ve often heard people say that 80% of data science involves cleaning the data before you even start to do the analysis. As we saw this week, that’s definitely the case. Moreover, even after you’ve started, you often need to double back and make corrections.

Questions? Comments? Suggestions? Share them with me, and others!

Meanwhile, my Jupyter notebook for this week is here: [https://drive.google.com/file/d/17\_44B6aiSdaAD1hk8ZEdR-aPlK9XdwN\_/view?usp=sharing](https://drive.google.com/file/d/17%5F44B6aiSdaAD1hk8ZEdR-aPlK9XdwN%5F/view?usp=sharing&ref=bambooweekly.com)

I’ll be back on Wednesday with another problem from current events.

Reuven