Skip to content

pandas dt.day_name

Turn datetime values into the words people actually use — Monday, September — and then fight to keep them in calendar order.

What they do

Have you ever grouped ridership by day of the week, gotten a perfectly correct answer, and then stared at a chart that ran Friday, Monday, Saturday, Sunday, Thursday, Tuesday, Wednesday? Every number was right. The labels were right. The order was nonsense.

That is the whole story of these two methods, and why I teach them together. dt.day_name() gives you the name of the weekday for every value in a datetime column, and dt.month_name() gives you the name of the month. Both are the readable cousins of the numeric attributes: dt.day_of_week returns 0 through 6, and I have never once remembered whether 0 is Monday or Sunday, while dt.month returns 1 through 12, which is fine for arithmetic and useless as an axis label.

The catch is that the moment you swap a number for a name, you throw away the ordering that the number carried for free. Pandas does not know that Monday comes before Tuesday. It knows that 'Friday' sorts before 'Monday', because F comes before M. Getting the names is one line. Getting them back in calendar order is the part of this page worth reading twice.

Official documentation: Series.dt.day_name and Series.dt.month_name.

The arguments that earn their keep

s.dt.day_name()               # 'Sunday', 'Monday', …
s.dt.month_name()             # 'January', 'February', …
s.dt.day_name(locale='fr_FR') # 'Dimanche', 'Lundi', …

There is exactly one argument, locale, and it hands the naming off to your operating system's locale database — so the locale='fr_FR' that works on my laptop can raise on a stripped-down Linux container that never installed it.

Both methods also live on a DatetimeIndex, where you drop the .dt: df.index.month_name().

If you want something these two do not give you — an abbreviated Sun, or a day name glued to a date in one string — that is .dt.strftime(), where %A is the full name and %a the short one. I build those format strings at strfti.me, which shows you the result as you type instead of making you remember which letter means what.

A worked example, on real data

The MTA publishes daily ridership and traffic for New York City, one row per day per mode of transit, going back to March 2020. It is the dataset behind Bamboo Weekly #152, and because it keeps growing, your numbers will run a little past mine; I ran these in August 2026.

import pandas as pd

url = ('https://data.ny.gov/api/views/sayj-mze2/'
       'rows.csv?accessType=DOWNLOAD')

df = pd.read_csv(url, parse_dates=['Date'])

df['Date'].dt.day_name().head(3)
0    Sunday
1    Sunday
2    Sunday
Name: Date, dtype: str

So far, so easy. Now the real question: how does ridership vary across the week?

(
    df
    .loc[lambda df_: df_['Mode'] == 'Subway']
    .assign(day=lambda df_: df_['Date'].dt.day_name())
    .groupby('day')['Count'].mean().round()
)
day
Friday       3071317.0
Monday       2876291.0
Saturday     2105098.0
Sunday       1664042.0
Thursday     3255007.0
Tuesday      3250657.0
Wednesday    3307635.0
Name: Count, dtype: float64

Seven correct numbers in an order no human being would choose. groupby sorts its keys, the keys are strings, and strings sort alphabetically. Hand that to plot.bar and you have published a chart that says Friday is the start of the week.

Getting Sunday back in front

There are two fixes, and the one you want depends on whether the ordering has to survive the rest of the chain.

The quick fix is to reindex. The order you want is yours to decide, not something Pandas can infer, so state it. I start my weeks on Sunday:

days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday',
        'Thursday', 'Friday', 'Saturday']

(
    df
    .loc[lambda df_: df_['Mode'] == 'Subway']
    .assign(day=lambda df_: df_['Date'].dt.day_name())
    .groupby('day')['Count'].mean().round()
    .reindex(days)
)
day
Sunday       1664042.0
Monday       2876291.0
Tuesday      3250657.0
Wednesday    3307635.0
Thursday     3255007.0
Friday       3071317.0
Saturday     2105098.0
Name: Count, dtype: float64

Now you can read it as a week: from the Sunday low, ridership jumps on Monday, peaks on Wednesday, eases off through Friday, and drops again on Saturday. Sunday runs at half of Wednesday.

The better fix is to teach the column its own order once, by making it an ordered Categorical, and then never think about it again:

day_dtype = pd.CategoricalDtype(days, ordered=True)

(
    df
    .loc[lambda df_: df_['Mode'] == 'Subway']
    .assign(day=lambda df_: df_['Date'].dt.day_name().astype(day_dtype))
    .groupby('day', observed=True)['Count'].mean().round()
)

That returns the same seven rows in the same order, without the trailing reindex. The difference is that the knowledge now lives in the data rather than in one method call: every subsequent sort_values, sort_index, and pivot_table respects it.

One warning if you build a Sunday-first Categorical like this one: dt.dayofweek does not move with you. It numbers Monday 0 and Sunday 6 whatever your categories say. Sort by the name or by the number, but do not mix the two in one chain and expect them to agree.

value_counts shows off the difference. Asking which days the subway carries more than four million riders gives you frequency order, a different kind of wrong:

(
    df
    .loc[lambda df_: (df_['Mode'] == 'Subway') & (df_['Count'] > 4_000_000)]
    ['Date'].dt.day_name()
    .value_counts()
)
Date
Wednesday    107
Thursday     103
Tuesday       87
Friday        37
Monday        20
Name: count, dtype: int64

Convert first and the same call sorts into the week, and — the part I like — tells you about the days that never appear at all:

(
    df
    .loc[lambda df_: (df_['Mode'] == 'Subway') & (df_['Count'] > 4_000_000)]
    ['Date'].dt.day_name().astype(day_dtype)
    .value_counts().sort_index()
)
Date
Sunday         0
Monday        20
Tuesday       87
Wednesday    107
Thursday     103
Friday        37
Saturday       0
Name: count, dtype: int64

The plain value_counts simply omitted Sunday and Saturday. The Categorical version reports them as zero, bookending the week, and that is the actual finding: in six and a half years, the subway has never carried four million riders on a weekend.

Months work identically, with twelve names instead of seven:

months = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
          'August', 'September', 'October', 'November', 'December']
month_dtype = pd.CategoricalDtype(months, ordered=True)

(
    df
    .loc[lambda df_: df_['Mode'] == 'Subway']
    .assign(month=lambda df_: df_['Date'].dt.month_name().astype(month_dtype),
            year=lambda df_: df_['Date'].dt.year)
    .pivot_table(index='month', columns='year', values='Count',
                 aggfunc='mean', observed=True)
    .loc[:, 2023:2025]
    .round()
)
year            2023       2024       2025
month
January    2870945.0  2958436.0  3188048.0
February   3010695.0  3114675.0  3361130.0
March      3259055.0  3221477.0  3494515.0
April      3139964.0  3331715.0  3666954.0
May        3368876.0  3416232.0  3627925.0
June       3248567.0  3225398.0  3551575.0
July       2928968.0  3031862.0  3424240.0
August     3032171.0  2977661.0  3303441.0
September  3177674.0  3452625.0  3690550.0
October    3364393.0  3655181.0  3737087.0
November   3282908.0  3418435.0  3548921.0
December   3125408.0  3353056.0  3567151.0

A seasonal shape down the column, a recovery across the row. Drop the astype and the table starts at April and ends at September.

Four mistakes people make

Calling it on a column that is still strings. The error message names the problem precisely. Forget parse_dates and your dates are text:

pd.read_csv(url)['Date'].dt.day_name()
AttributeError: Can only use .dt accessor with datetimelike values

The fix is upstream: parse_dates= in read_csv, or to_datetime after the fact. When in doubt, check df.dtypes.

Forgetting the parentheses. dt.year is data, so it takes no parentheses. dt.day_name is a method, so it needs them. The inconsistency is real, and this is the one mistake here that does not announce itself:

(
    df
    .loc[lambda df_: df_['Mode'] == 'Subway']
    .assign(day=lambda df_: df_['Date'].dt.day_name)
    .head(2)
)
         Date    Mode      Count                                      day
6  2020-03-01  Subway  2212965.0  <bound method DatetimeArray.day_name...
13 2020-03-02  Subway  5329915.0  <bound method DatetimeArray.day_name...

No exception. You get a column containing the same method object 2,364 times, and if you group by it, all your rows land in a single group with a truly memorable name. Going the other way is louder: dt.year() raises TypeError: 'Series' object is not callable. Names are methods, numbers are attributes.

Plotting whatever order value_counts handed you. value_counts sorts by frequency and groupby sorts alphabetically, and neither one is the week or the year. Nothing in the output looks broken, which is exactly why this ships.

Building the Categorical from what happens to be in the data. pd.CategoricalDtype(sorted(df['day'].unique())) feels clever and lands you right back in alphabetical order; taking the values in the order they appear gets you whatever day the file starts on. Type the seven names out.

Where it shows up in Bamboo Weekly

Eight Bamboo Weekly solutions reach for one of these two methods on real data. Four are worth studying:

Bamboo Weekly #162: Spotify and car accidents is the clearest statement of why the names beat the numbers. Checking whether hit albums all drop on the same weekday, dt.day_name() plus value_counts answers the question in two lines. Nine of ten albums came out on a Friday.

Bamboo Weekly #152: Congestion pricing uses the same MTA file as above, pairing dt.day_name() with isin to build an is_weekend column — a far more readable weekend test than comparing day numbers.

Bamboo Weekly #134: Taiwan weather walks straight into the ordering problem and solves it a third way. Putting dt.month_name() into the columns of a pivot_table sorted the months alphabetically, so I passed sort=False to keep the file's own order — which works when the source is already in calendar order, and is a reminder that the Categorical is not the only tool.

Bamboo Weekly #108: Measles is where I spell out the parentheses rule, in a chain that calls month_name() on a DatetimeIndex — no .dt needed — right next to a bare .year. As I noted there: if you find this inconsistent and confusing, you are not alone.

Practice it

Work through a dt.day_name exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/dt-day-name/

Go deeper

The numeric siblings are dt.month and dt.year, and you will often want one of each — a number to sort by, a name to display. Everything on this page assumes a real datetime column, which means to_datetime comes first, and the ordering fixes lean on astype and sort_index. The Pandas user guide chapters on time series and categorical data are the canonical treatments of the two halves of this page.

More Pandas videos on Python and Pandas with Reuven Lerner.

Bamboo Weekly is the practice. If you want the structured version — full courses with downloadable Jupyter notebooks, plus live Pandas office hours when you get stuck — that is LernerPython+Data. A paid Bamboo Weekly subscription is included with it.

See it on real data

Below are the 6 Bamboo Weekly exercises that use dt.day_name on real-world data — try each one, then study the worked solution.

Part of the Pandas Methods Index. See also practice by skill.