Home > Blockchain >  Custom non linear date x scale
Custom non linear date x scale

Time:08-15

I have data over time (consumption of gas, water, electricity) that I want to plot - from the past to today. But newer data is more important than older data. This is what I want as xaxis:

  • Data from today till back one week - daily
  • Data from one week till three weeks back - weekly
  • Data older, monthly

The xticks would then would - roughly - look like this: 2022-05 2022-06 2022-07 2022-07-25 2022-08-01 2022-08-08 2022-08-09 2022-08-10 2022-08-11 AND would be equally spaced!. This would give me a good overview what happened yesterday, last week, and "in the entire past". However, I pretty much got stuck with the forward and reverse functions and the return values. Does anyone have a example? I got this far only:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates

def forward(x):
    return x
def reverse(x):
    return x

rng = pd.date_range(end='2022-08-14', periods=180, freq='D')

df = pd.DataFrame({'Date': rng, 'Val': np.random.randn(len(rng)) }) 
df.set_index("Date",inplace=True)
ax=df.plot()
plt.gca().set_xscale('function', functions=(forward, reverse))

ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
plt.xticks(rotation = 30)
plt.show()

CodePudding user response:

import numpy as np

idx = np.full(df.shape[0], False)
idx[-7:] = True
idx[-7:-3*7:-7] = True
idx[-3*7::-30] = True

df2 = df[idx]
print(df)
Date Val
2022-02-25 00:00:00 1.1248252961101586
2022-03-27 00:00:00 0.04203875889903905
2022-04-26 00:00:00 0.5896163070537574
2022-05-26 00:00:00 -1.8329001623876962
2022-06-25 00:00:00 -1.0173128750157898
2022-07-25 00:00:00 -0.6708114840150123
2022-08-01 00:00:00 0.2657251487401089
2022-08-08 00:00:00 -0.6617901994374795
2022-08-09 00:00:00 0.17380650386233432
2022-08-10 00:00:00 -0.13383053082508994
2022-08-11 00:00:00 -1.1532937501757903
2022-08-12 00:00:00 -0.5940972756876102
2022-08-13 00:00:00 -0.9253845051895224
2022-08-14 00:00:00 -0.5568183429382052
import matplotlib.pyplot as plt

plt.plot(df2.index.astype(str), df2.Val);
plt.xticks(rotation=45);

plot

CodePudding user response:

Creating a custom scale for matplotlib

Two transformations are needed:

  1. Compress each of the previous 3 weeks into the length of a day.
  2. Compress each months, except the latest, into the length of a day.

Before any activity, let's simplify the data. I do it to avoid any hassle with datetime:

x = np.arange(120)
y = x 

The main line is simple - if a date is earlier some endpoint then push it forward at the given scale. Since we compress data twice in the same manner, let's make a factory:

def compress_factory(endpoint, scale):
    return lambda x: scale * x   (1-scale) * endpoint

Illustration for the formula above
illustration

Now, prepare two pair of supportive functions, for the week and month:

w_ago = x[-1] - 7
w_forward = compress_factory(w_ago, 1/7)
w_backward = compress_factory(w_ago, 7)

m_ago = x[-1] - 30
leap = w_forward(m_ago) - m_ago
m_forward = lambda x: compress_factory(m_ago, 1/30)(x)   leap
m_backward = lambda x: compress_factory(m_ago, 30)(x - leap)

We apply them in two consequent np.where to separate scaling of the corrent week, previous 3 weeks and all the other dates:

def forward(x):
    return np.where(
        x > w_ago, 
        x,
        np.where(
            x > m_ago, 
            w_forward(x), 
            m_forward(x)
        )
    )

def backward(x):
    return np.where(
        x > w_ago, 
        x,
        np.where(
            x > w_forward(m_ago), 
            w_backward(x), 
            m_backward(x)
        )
    )

Take into account that in backward we compare data to transformed month endpoint. The first comparison stay the same because we don't scale the latest week.

Two sanity tests:

# 1. Does backward(forward(x)) equal x?
assert np.isclose(backward(forward(x)), x).all()
# 2. Is forward monotonic?
assert (((_:=forward(x))[1:] - _[:-1]) >= 0).all()

Let's see how it looks on simplified data:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt


x = np.arange(1, 91)
y = x  


def compress_factory(endpoint, size):
    return lambda x: size * x   (1-size) * endpoint

w_size = 7
w_ago = x[-1] - w_size
w_forward = compress_factory(w_ago, 1/w_size)
w_backward = compress_factory(w_ago, w_size)

m_size = 30
m_ago = x[-1] - m_size
leap = w_forward(m_ago) - m_ago
m_forward = lambda x: compress_factory(m_ago, 1/m_size)(x)   leap
m_backward = lambda x: compress_factory(m_ago, m_size)(x - leap)


def forward(x):
    return np.where(
        x > w_ago, 
        x,
        np.where(
            x > m_ago, 
            w_forward(x), 
            m_forward(x)
        )
    )


def backward(x):
    return np.where(
        x > w_ago, 
        x,
        np.where(
            x > w_forward(m_ago), 
            w_backward(x), 
            m_backward(x)
        )
    )


assert np.isclose(backward(forward(x)), x).all()
assert (((_:=forward(x))[1:] - _[:-1]) >= 0).all()


fig, ax = plt.subplots(figsize=(16,5))
ax.plot(x, y)
ax.grid()
ax.set_xscale('function', functions=(forward, backward))
ax.set_xbound(min(x), max(x))
ax.set_ybound(min(y), max(y))

Plot with custom scaling
plot with custom scale

  • Related