7

In R, it's possible to format POSIXlt date-time objects as a month:

format(Sys.time(), format='%Y-%m')

Is there a way to do the same thing with seasons, or 3-month groups (DJF, MAM, JJA, SON)? These divisions are really common in climatological and ecological science, and it would be great to have a neat way to format them quickly like with months. Obviously DJF falls over 2 years, but for the purposes or this question, that doesn't really matter - just consistently shove them into either year, (or, ideally, it would be good to be able to specify which year they go into).

I'm using the output as a index for by(), so the output format doesn't matter much, just as long as each year/season is unique.

Edit: example data:

dates <- Sys.Date()+seq(1,380, by=35)
dates <- structure(c(16277, 16312, 16347, 16382, 16417, 16452, 16487, 
                     16522, 16557, 16592, 16627), class = "Date")
dates
#[1] "2014-07-26" "2014-08-30" "2014-10-04" "2014-11-08" "2014-12-13"
# "2015-01-17" "2015-02-21" "2015-03-28" "2015-05-02" "2015-06-06" "2015-07-11"

should result in:

c("2014-JJA", "2014-JJA", "2014-SON", "2014-SON", "2015-DJF", "2015-DJF", 
  "2015-DJF", "2015-MAM", "2015-MAM", "2015-JJA", "2015-JJA")

But the "2015-DJF"s could also be "2014-DJF". Also, the form of the output doesn't matter - "2104q4 or 201404 would also be fine.

thelatemail
  • 91,185
  • 12
  • 128
  • 188
naught101
  • 18,687
  • 19
  • 90
  • 138
  • 1
    [Here's](http://stackoverflow.com/questions/9500114/find-which-season-a-particular-date-belongs-to/9501225#9501225) a related question and answer. – Josh O'Brien Jul 25 '14 at 04:04
  • 2
    If Q1 is DJF, etc. then convert it to `"yearmon"` class, add one month and convert it to `"yearqtr"` class: `library(zoo); format(as.yearqtr(as.yearmon(Sys.time())+1/12))` . You can use a format string if you want a different format, e.g. `format(as.yearqtr(as.yearmon(Sys.time())+1/12), "%Y-%q")` – G. Grothendieck Jan 19 '16 at 14:50

4 Answers4

6

as.POSIXlt returns a named list (which makes it unsuitable for data.frame columns). The list columns can be individually accessed and include "year" (1900-based, unlike 1970 used for default) and "mon" (0-based). Best place to see this list in hte help system is ?DateTimeClasses:

First just a Seasons calculation, then a Year-Seasons calculation

 c('DJF', 'MAM', 'JJA', 'SON')[ # select from character vector with numeric vector
          1+((as.POSIXlt(dates)$mon+1) %/% 3)%%4]

 [1] "JJA" "JJA" "SON" "SON" "DJF" "DJF" "DJF" "MAM" "MAM" "JJA"
[11] "JJA"



   paste( 1900 + # this is the base year for POSIXlt year numbering 
             as.POSIXlt( dates )$year + 
             1*(as.POSIXlt( dates )$year==12) ,   # offset needed for December
          c('DJF', 'MAM', 'JJA', 'SON')[          # indexing from 0-based-mon
                             1+((as.POSIXlt(dates)$mon+1) %/% 3)%%4] 
          , sep="-")
 [1] "2014-JJA" "2014-JJA" "2014-SON" "2014-SON" "2014-DJF"
 [6] "2015-DJF" "2015-DJF" "2015-MAM" "2015-MAM" "2015-JJA"
[11] "2015-JJA"

Shouldn't be that difficult to make a function that constructs the formatting you expect. This is just modulo arithmetic on the POSIXlt values for month and year.

IRTFM
  • 258,963
  • 21
  • 364
  • 487
  • Cool, nice one. I got started on this, and then saw the answer that you posted then deleted, so I wrote my own in the mean time. Yours is better in that it uses nicer season names, but I guess they're functionally more or less equivalent. I'll let the votes decide :) – naught101 Jul 25 '14 at 04:08
  • I don't think mine is very obvious. I got snarled in knots with the modulo divsions and remainders always coming out wrong. – IRTFM Jul 25 '14 at 04:20
  • So did I, because the frigging internal representation of months in POSIXlt is 0-indexed, while the external representation is 1-indexed. Crazy. Anyway, I decided to go with mine, but only because it allows you to leave the results as dates, which is useful for my purposes. – naught101 Jul 25 '14 at 04:23
  • Can anyone shed some light on said modulo arithmetic? I am interested but confused by: `%/%` and `%%` here. – boshek Jun 13 '16 at 19:04
  • You should 'play' with 1:12 using %% and %/%. – IRTFM Jun 14 '16 at 02:19
  • 1
    @KingJulien: Please don't spam the comments. Your question will get answered eventually. – naught101 Aug 20 '18 at 04:30
4

I like using a lookup vector for these sorts of problems, e.g.:

x <- as.POSIXlt(
  seq.Date(as.Date("2000-01-01"),as.Date("2002-01-01"),by="2 months")
)

E.g., if you want to specify southern hemisphere seasons, you could do:

src <- rep(c("su","au","wi","sp"),each=3)[c(2:12,1)]

paste(format(x,"%Y-%m"),src[x$mon+1])
# [1] "2000-01 su" "2000-03 au" "2000-05 au" "2000-07 wi" "2000-09 sp"
# [6] "2000-11 sp" "2001-01 su" "2001-03 au" "2001-05 au" "2001-07 wi"
#[11] "2001-09 sp" "2001-11 sp" "2002-01 su"

Change the src names as you see fit to relabel the categories.

thelatemail
  • 91,185
  • 12
  • 128
  • 188
2

Let Q1 be DJF; Q2, MAM; etc. then:

seasonal.quarters <- function(x) {
    x <- as.POSIXlt(x)
    x$mon <- (x$mon + 1) %% 12
    quarters(x)
}

options(stringsAsFactors=FALSE)

nonleap.year <- seq(from=as.POSIXct('2013-1-1'), to=as.POSIXct('2014-1-1'), by='day')
d <- data.frame(ms=months(nonleap.year), qs=seasonal.quarters(nonleap.year))
by(d, INDICES=list(d$qs), FUN=function(x) unique(x$ms))
# : Q1
# [1] "January"  "February" "December"
# -------------------------------------
# : Q2
# [1] "March" "April" "May"  
# -------------------------------------
# : Q3
# [1] "June"   "July"   "August"
# -------------------------------------
# : Q4
# [1] "September" "October"   "November" 

leap.year <- seq(from=as.POSIXct('2016-1-1'), to=as.POSIXct('2017-1-1'), by='day')
d <- data.frame(ms=months(leap.year), qs=seasonal.quarters(leap.year))
by(d, INDICES=list(d$qs), FUN=function(x) unique(x$ms))
# : Q1
# [1] "January"  "February" "December"
# -------------------------------------
# : Q2
# [1] "March" "April" "May"  
# -------------------------------------
# : Q3
# [1] "June"   "July"   "August"
# -------------------------------------
# : Q4
# [1] "September" "October"   "November" 
Matthew Plourde
  • 43,932
  • 7
  • 96
  • 113
  • The difftime basically turns December into January, right? But this won't work accurately, because each quarter is a different length, and the first quarter is a different length in leap-years. – naught101 Jul 25 '14 at 02:41
  • Yeah, I just realized that. This isn't reliable. Will remove shortly. – Matthew Plourde Jul 25 '14 at 02:43
  • Not really sure if this is doing what I'm asking - see the edited-in example in the question. – naught101 Jul 25 '14 at 05:58
2

This is an alternative to 42-'s answer above. Go see my comment there for the reason I posted it.

dates_orig <- as.POSIXlt(c("2013-01-01", "2013-02-01", "2013-03-01", "2013-04-01", "2013-05-01", "2013-06-01", "2013-07-01", "2013-08-01", "2013-09-01", "2013-10-01", "2013-11-01", "2013-12-01", "2014-01-01", "2014-02-01", "2014-03-01", "2014-04-01", "2014-05-01", "2014-06-01", "2014-07-01", "2014-08-01", "2014-09-01", "2014-10-01", "2014-11-01", "2014-12-01"))

format(dates_orig, format='%Y%b')
 [1] "2013Jan" "2013Feb" "2013Mar" "2013Apr" "2013May" "2013Jun" "2013Jul" "2013Aug" "2013Sep" "2013Oct" "2013Nov" "2013Dec" "2014Jan" "2014Feb" "2014Mar"
[16] "2014Apr" "2014May" "2014Jun" "2014Jul" "2014Aug" "2014Sep" "2014Oct" "2014Nov" "2014Dec"

dates <- as.POSIXlt(dates_orig)
# shift Jan and Feb to the previous year
dates$year[dates$mon < 2] <- dates$year[dates$mon < 2] - 1
# convert months to seasons (named by first month of season)
dates$mon <- (((dates$mon - 2) %/% 3) %% 4) * 3 + 2

format(dates, format='%Y%b')
[1] "2012Dec" "2012Dec" "2013Mar" "2013Mar" "2013Mar" "2013Jun" "2013Jun" "2013Jun" "2013Sep" "2013Sep" "2013Sep" "2013Dec" "2013Dec" "2013Dec" "2014Mar"
[16] "2014Mar" "2014Mar" "2014Jun" "2014Jun" "2014Jun" "2014Sep" "2014Sep" "2014Sep" "2014Dec"
naught101
  • 18,687
  • 19
  • 90
  • 138
  • 1
    It uses a destructive modification to the original and then it doesn't really answer the question because you are still getting months. – IRTFM Sep 25 '16 at 15:43
  • @42-: it results in the first month of the season - it's not hard to string-replace the months for "DJF", "MMA", etc. if necessary. It wasn't for me. As for data loss, converting dates to seasons is *always* going to be lossy. If you care, then don't discard the original data. Your solution is equally lossy. – naught101 Sep 26 '16 at 01:31
  • You should edit that explanation into the answer instead of pointing people at comments, I think. – Frank Sep 26 '16 at 01:40
  • Part of my comment came from a misreading, but you answered the other part.I suggest you carry forward with Frank's suggestion and I'll delete all mine. – IRTFM Sep 26 '16 at 16:33