<jupyter_start><jupyter_text>Look up <NAME> and what info we can get for him<jupyter_code>id = playerid_lookup('lynn','lance')
id
full_df=statcast_pitcher(start_dt="2011-01-01", end_dt="2019-06-14", player_id=458681)
full_df.head()<jupyter_output><empty_output><jupyter_text>Okay, I'd like to look at pitch type by year, so break this dataframe down year-wise<jupyter_code>full_df['year'] = full_df['game_date'].str[:4].astype(int)
piv_dict = {}
for year in range(2011,2020):
    this_year_df = full_df[full_df["year"] == year]
    piv_dict[year] = this_year_df["pitch_name"].value_counts(normalize=True)

new_df = pd.DataFrame(piv_dict).transpose()
new_df = new_df.drop(["Intentional Ball","Pitch Out","Fastball"], axis=1)
new_df.head()<jupyter_output><empty_output><jupyter_text>Plot pitch breakdown, both as line chart to see progression and stack plot for parts of a whole<jupyter_code>fig=plt.figure(figsize=(10,5))
new_df.plot(marker='o', ax=plt.gca())
plt.legend(frameon=False, bbox_to_anchor=(1, 0.65),fontsize=13)
plt.xlabel("Year",fontsize=14)
plt.ylabel("Fraction of Total Pitches", fontsize=14)
plt.gca().tick_params(axis='both', which='major', labelsize=14)
plt.tight_layout()
plt.savefig('../plots/lynn/lynn_line')
fig=plt.figure(figsize=(8,6))
new_df.plot(kind='bar',stacked=True, ax=plt.gca())
plt.legend(frameon=False, bbox_to_anchor=(1, 0.65),fontsize=13)
plt.xlabel("Year",fontsize=14)
plt.ylabel("Fraction of Total Pitches", fontsize=14)
plt.gca().tick_params(axis='both', which='major', labelsize=14)
plt.tight_layout()
plt.savefig('../plots/lynn/lynn_stack')<jupyter_output><empty_output><jupyter_text>Next, I'm interested to see his production rate - WAR/IP. WAR isn't readily available in statcast, so I'm going to have to webscrape it from fangraphs<jupyter_code>from bs4 import BeautifulSoup
import requests
lynn_page = 'https://www.fangraphs.com/statss.aspx?playerid=2520&position=P'
page = requests.get(lynn_page)
soup = BeautifulSoup(page.text, 'html.parser')
found = soup.find('table',{'class':'rgMasterTable', 'id':'SeasonStats1_dgSeason11_ctl00'})
stats_df=pd.read_html(str(found))[0]<jupyter_output><empty_output><jupyter_text>clean up this table<jupyter_code># get rid of minor league, projections
valid_teams = ['Cardinals','2 Teams','Rangers']
stats_df= stats_df[stats_df['Team'].isin(valid_teams)]
#get rid of playoff stats
season_rows = stats_df['Season'].loc[(stats_df['Season'].shift() != stats_df['Season'])]
stats_df = stats_df[stats_df.index.isin(season_rows.index)]
stats_df['Season']=stats_df['Season'].astype(int)
stats_df = stats_df.set_index('Season')
# Convert IP since .1 and .2 used for partial innings
extra_outs = stats_df['IP'].astype(str).str[-1]
total_outs = stats_df['IP'].apply(np.floor) * 3 + extra_outs.astype(int)
stats_df['corrected_IP'] = total_outs/3
stats_df['IP per WAR'] = (stats_df['corrected_IP']/stats_df['WAR'])
fig = plt.figure(figsize=(8,5))
(stats_df['corrected_IP']/stats_df['WAR']).plot(marker='o',linestyle='none')
plt.xlim(2010.5, 2019.5)
plt.xlabel("Season",fontsize=14)
plt.ylabel("IP per WAR", fontsize=14)
plt.gca().tick_params(axis='both', which='major', labelsize=14)
plt.savefig('../plots/lynn/ip_per_war')<jupyter_output><empty_output><jupyter_text>There's an interesting trend here, if you get rid of his outlier and rescale...<jupyter_code>fig = plt.figure(figsize=(8,5))
(stats_df['corrected_IP']/stats_df['WAR']).plot(marker='o',linestyle='none')
plt.xlim(2010.5, 2019.5)
plt.ylim(30,80)
plt.xlabel("Season",fontsize=14)
plt.ylabel("IP per WAR", fontsize=14)
plt.gca().tick_params(axis='both', which='major', labelsize=14)
#plt.savefig('../plots/lynn/lynn_ip_per_war_small')<jupyter_output><empty_output><jupyter_text>It'd be nice to get rid of the gap year and fit this...<jupyter_code>pre2019 = stats_df[~stats_df.index.isin([2019,2017,2016])]
pre2019 = pre2019.reset_index()
pre2019 = pre2019.reset_index()
fig = plt.figure(figsize=(8,5))

import scipy.stats
slope, intercept, r_value, p_value, std_err = \
 scipy.stats.linregress(x=pre2019.index,y=pre2019['IP per WAR'])
rsquared = r_value**2
print("r^2 = {0}".format(rsquared))

pre2019['IP per WAR'].plot(marker='o', linestyle='none')

plt.xlim(0, 6.5)
valid_years = [str(x) for x in list(range(2011,2020)) if x not in [2017,2016]]
locs, labels = plt.xticks()
plt.xticks(ticks=locs,labels=valid_years)
plt.xlim(-0.5, 6.5)

plt.ylim(25,80)

xlims = plt.xlim()
new_x = np.arange(xlims[0], xlims[1],(xlims[1]-xlims[0])/250.)
plt.gca().plot(new_x, intercept + slope *  new_x, color='g', linestyle='--', lw = 1)
plt.annotate('$r^2 = {0:.2f}$'.format(rsquared),xy=(0.99,0.5),xycoords='axes fraction', ha='right',fontsize=14, color='g',rotation=-13)
plt.xlabel("Season",fontsize=14)
plt.ylabel("IP per WAR", fontsize=14)
plt.gca().tick_params(axis='both', which='major', labelsize=14)

#fig = plt.figure(figsize=(8,5))
#sns.regplot(x='index',y='IP per WAR', data=pre2019)
plt.plot(6,stats_df['IP per WAR'][2019], marker='o',color='firebrick')
plt.savefig('../plots/lynn/lynn_ip_per_war_regression')<jupyter_output>r^2 = 0.896171713851328<jupyter_text>2019 does not match the trend, let's figure out what the expectation for that might be...<jupyter_code>print("Regressed value: {0}".format(round(slope*6+ intercept,2)))
print("pre-2019 Career average: {0}".format(round(pre2019['IP per WAR'].mean(),2)))
print("Actual value: {0}".format(round(stats_df['IP per WAR'][2019],2)))<jupyter_output>Regressed value: 50.31
pre-2019 Career average: 61.2
Actual value: 29.66<jupyter_text>As one last check - he's changed his pitches, but do those pitches actually have better results overall than the 2-seamer he moved away from?<jupyter_code>results = list(full_df["description"].value_counts().keys())
pitchtypes = list(full_df["pitch_name"].value_counts().keys())

piv_dict = {}
for p in pitchtypes:
    p_df = full_df[full_df["pitch_name"] == p]
    piv_dict[p] = p_df["description"].value_counts(normalize=True)
    
# Clean and add a total strike row
result_df_edit = pd.DataFrame(piv_dict).transpose().fillna(0)    
result_df_edit["all_strikes"] = result_df_edit["called_strike"] + result_df_edit["swinging_strike"] +  result_df_edit["swinging_strike_blocked"] 
result_df = result_df_edit.transpose()
result_df<jupyter_output><empty_output><jupyter_text>Calculate improvement in strikes and balls in play<jupyter_code>print("More strikes:")
print("Cutter: ", result_df["Cutter"].loc["swinging_strike"] - result_df["2-Seam Fastball"].loc["swinging_strike"])
print("4-seam: ", result_df["4-Seam Fastball"].loc["swinging_strike"] - result_df["2-Seam Fastball"].loc["swinging_strike"])

print("Less in play:")
print("Cutter: ",  result_df["2-Seam Fastball"].loc["hit_into_play"]-result_df["Cutter"].loc["hit_into_play"])
print("4-seam: ",  result_df["2-Seam Fastball"].loc["hit_into_play"]-result_df["4-Seam Fastball"].loc["hit_into_play"])<jupyter_output>Less in play:
Cutter:  0.02026823536730897
4-seam:  0.04336006618719038<jupyter_text>Look at when he's throwing pitches<jupyter_code>piv_dict = {}
for s in range(0,3):
    p_df = full_df[full_df["strikes"] == s]
    p_df = p_df[p_df['year'] != 2019]
    piv_dict[s] = p_df["pitch_name"].value_counts(normalize=True)


# Clean and add a total strike row
result_df_edit = pd.DataFrame(piv_dict).transpose().fillna(0)    
result_df_previous = result_df_edit[["2-Seam Fastball", "4-Seam Fastball", "Changeup", "Curveball", "Cutter"]].transpose()
result_df_previous.rename_axis("Strikes",axis=1)
piv_dict = {}
for s in range(0,3):
    p_df = full_df[full_df["strikes"] == s]
    p_df = p_df[p_df['year'] == 2019]
    piv_dict[s] = p_df["pitch_name"].value_counts(normalize=True)

# Clean and add a total strike row
result_df_edit = pd.DataFrame(piv_dict).transpose().fillna(0)    
result_df_2019 = result_df_edit[["2-Seam Fastball", "4-Seam Fastball", "Changeup", "Curveball", "Cutter"]].transpose()
result_df_2019.rename_axis("Strikes",axis=1)

sub_df=result_df_2019.subtract(result_df_previous).round(3)
sub_df=sub_df.rename_axis("Strikes",axis=1)
sub_df
sub_df.to_csv('../plots/lynn/usagetable.csv')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.17584719654959952, 'fraction_numerical': 0.0452248921749846, 'mean_word_length': 7.812160694896852, 'pattern_counts': {'":': 0, '<': 43, '<?xml version=': 0, '>': 43, 'https://': 1, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '162710', 'license': 'MIT', 'n_tokens_mistral': 3453, 'n_tokens_neox': 3168, 'n_words': 519, 'path': 'notebooks/LanceLynn2019.ipynb', 'repo_name': 'tjburch/baseball-studies', 'size': '8119'}